人工智能协会第二次课

人工智能协会第二次课

学习Python,这一篇管够(入门|基础|进阶|实战)

Python基础(中)

一、Python 集合

Python也包含有 集合 类型。集合是由不重复元素组成的无序的集。它的基本用法包括成员检测和消除重复元素。集合对象也支持像 联合,交集,差集,对称差分等数学运算。

集合结构如下:

set1 = {'hello', 'hello', 'word', 'word'}set1# 输出结果实现自动去重{'hello', 'word'}

1、集合创建

可以使用大括号 { } 或者 set() 函数创建集合,

创建格式:

parame = {value01,value02,...}或者set(value)

注意:创建一个空集合必须用 set() 而不是 { },因为 { } 是用来创建一个空字典。

# 创建空集合
empty_set = set()
type(empty_set)  

<class 'set'>

# 创建空字典
empty_dict = {}
type(empty_dict) 
<class 'dict'>

2、集合的基本操作

2.1 添加元素

语法格式:

s.add(x)

将元素 x 添加到集合 s 中,如果元素已存在,则不进行任何操作。

s = set(('hello','world'))
print(s)
# 向集合 s 中添加元素s.add('!')print('添加元素后的集合是:%s' % s)
# 输出结果是:添加元素后的集合是:{'world', '!', 'hello'}

除了 add() 方法可以添加元素外,还有一个方法,也可以添加元素,并且参数可以是列表,元组,字典等,语法格式如下:

s.update( x )

参数 x 可以是一个,也可以是多个,多个参数之间用逗号相隔

# 1)添加列表
s.update([1,3],[2,4])print('添加元素后的集合是:%s' % s)
# 2)添加元组
s.update(('h', 'j'))print('添加元素后的集合是:%s' % s)

2.2 移除元素

语法格式:

s.remove( x )

将元素 x 从集合 s 中移除,如果元素不存在,则会发生错误。

# 将元素 2 从集合中移除
s.remove(2)
print('移除元素 2 后的集合是:%s' % s)
# 如果移除集合中不存在的元素会报异常
# 移除集合中不存在的集合
s.remove('hi')
print('移除元素后的集合是:%s' % s)
# 异常信息
Traceback (most recent call last):  File "test.py", line 20, in <module>  s.remove('hi')  KeyError: 'hi'

此外还有一个方法也是移除集合中的元素,且如果元素不存在,不会发生错误。格式如下所示:

s.discard( x )
thisset = set(("Google", "Runoob", "Taobao"))
thisset.discard("Facebook")  
# 不存在不会发生错误
print(thisset){'Taobao', 'Google', 'Runoob'}

我们也可以设置随机删除集合中的一个元素,语法格式如下:

s.pop()
# 随机删除集合中的一个元素print(s)
s.pop()
print('移除元素后的集合是:%s' % s)
输出结果:{1, 3, 4, 'world', '!', 'hello', 'h', 'j'}移除元素后的集合是:{3, 4, 'world', '!', 'hello', 'h', 'j'}

注意:在交互模式,pop 是删除集合的第一个元素(排序后的集合的第一个元素)。

2.3 计算集合元素个数

语法格式:

len(s)

计算集合 s 元素个数。

print('集合 s 的长度是:%s' % len(s))
# 输出结果集合 s 的长度是:7

2.4 清空集合

语法格式:

s.clear()

清空集合 s

s.clear()
print('集合清空后的结果是:%s' % s)
# 输出结果:集合清空后的结果是:
set()

2.5 判断元素是否存在

语法格式:

x in s

判断元素 x 是否在集合 s 中,存在返回 True,不存在返回 False。

# 判断元素是否存在
s = {'hello',  'word'}
# 判断元素 hello 是否在集合 s 中
print(hello' in s)
# 输出结果:True

2.6 集合运算

集合之间的运算符分别是‘-’、‘|’、‘&’、‘^’ ; 下面以两个集合之间的运算为例进行讲解:

  • ‘-’:代表前者中包含后者中不包含的元素
  • ‘|’:代表两者中全部元素聚在一起去重后的结果
  • ‘&’:两者中都包含的元素
  • ‘^’:不同时包含于两个集合中的元素
a = set('afqwbracadaagfgbrafg')
b = set('rfgfgfalacazamddg')
a                                  
{'r', 'q', 'd', 'b', 'w', 'g', 'f', 'c', 'a'}>>> b{'r', 'd', 'g', 'f', 'l', 'z', 'c', 'm', 'a'}
# 集合a中包含而集合b中不包含的元素
a - b                              
{'b', 'w', 'q'} 
# 集合a或b中包含的所有元素
a | b                             
{'d', 'g', 'l', 'c', 'r', 'q', 'b', 'w', 'f', 'z', 'm', 'a'}
# 集合a和b中都包含了的元素
a & b                              
{'r', 'd', 'g', 'f', 'c', 'a'}
# 不同时包含于a和b的元素
a ^ b                             
{'l', 'q', 'b', 'w', 'z', 'm'}

3、集合推导式

和列表一样,集合也支持推导式

# 判断元素是否存在
a = {x for x in 'abracadabra' if x not in 'abc'}>>> a{'r', 'd'}

4、集合内置方法

4.1 difference()

difference() 方法用于返回集合的差集,即返回的集合元素包含在第一个集合中,但不包含在第二个集合(方法的参数)中,返回一个新的集合。** difference() 方法语法:**

set.difference(set)

实例: 两个集合的差集返回一个集合,元素包含在集合 x ,但不在集合 y :

# 求两个集合的差集,元素在 x 中不在 y 中
x = {"apple", "banana", "cherry"}y = {"google", "microsoft", "apple"}
z = x.difference(y)
print('两个集合的差集是:%s' % z)
# 输出结果为:{'cherry', 'banana'}

4.2 difference_update()

  • difference_update() 方法用于移除两个集合中都存在的元素。
  • difference_update() 方法与 difference() 方法的区别在于 difference() 方法返回一个移除相同元素的新集合,而 difference_update() 方法是直接在原来的集合中移除元素,没有返回值。
x = {"apple", "banana", "cherry"}y = {"google", "microsoft", "apple"}
x.difference_update(y)
print(x)结果为:{'banana', 'cherry'}

x1 = {1,2,3,4}y1 = {1,2,3}
x1.difference_update(y1)
print(x1)
# 结果为:{4}

4.3 intersection()

intersection() 方法用于返回两个或更多集合中都包含的元素,即交集,返回一个新的集合。

intersection() 方法语法:

set.intersection(set1, set2 ... etc)
**参数:**set1 -- 必需,要查找相同元素的集合set2 -- 可选,其他要查找相同元素的集合,可以多个,多个使用逗号 , 隔开

实例:

# 返回两个或者多个集合的交集
x = {"apple", "banana", "cherry"}y = {"google", "runoob", "apple"}
z = x.intersection(y)
print(z)
# 返回三个集合的交集
x = {"a", "b", "c"}y = {"c", "d", "e"}z = {"f", "g", "c"}
result = x.intersection(y, z)
print('三个集合的差集是:%s' % result)
# 输出结果:
{'apple'}两个集合的差集是:{'c'}

4.4 intersection_update()

  • intersection_update() 方法用于获取两个或更多集合中都重叠的元素,即计算交集。
  • intersection_update() 方法不同于 intersection() 方法,因为 intersection() 方法是返回一个新的集合,而 intersection_update() 方法是在原始的集合上移除不重叠的元素。

intersection_update() 方法语法:

set.intersection_update(set1, set2 ... etc)
**参数**set1 -- 必需,要查找相同元素的集合set2 -- 可选,其他要查找相同元素的集合,可以使用多个多个,多个使用逗号‘,’ 隔开

实例:

# 返回一个无返回值的集合交集
x = {"apple", "banana", "cherry"}y = {"google", "runoob", "apple"}
x.intersection_update(y)
print(x)
x = {"a", "b", "c"}y = {"c", "d", "e"}z = {"f", "g", "c"}
x.intersection_update(y, z)
print(x)
# 输出结果:{'apple'}{'c'}

4.5 union()

union() 方法返回两个集合的并集,即包含了所有集合的元素,重复的元素只会出现一次,返回值返回一个新的集合

语法:

union() 
方法语法:
set.union(set1, set2...)
参数set1 -- 必需,合并的目标集合set2 -- 可选,其他要合并的集合,可以多个,多个使用逗号 , 隔开。

实例:

# 合并两个集合,重复元素只会出现一次:
x = {"apple", "banana", "cherry"}y = {"google", "runoob", "apple"} z = x.union(y)  
print(z)
输出结果为:
{'cherry', 'runoob', 'google', 'banana', 'apple'}

# 合并多个集合:
实例 
1x = {"a", "b", "c"}y = {"f", "d", "a"}z = {"c", "d", "e"} result = x.union(y, z)  print(result)
输出结果为:
{'c', 'd', 'f', 'e', 'b', 'a'}

4.6 isdisjoint()

isdisjoint() 方法用于判断两个集合是否包含相同的元素,如果没有返回 True,否则返回 False。

语法:

isdisjoint() 
方法语法:
set.isdisjoint(set)

实例:

x = {"apple", "banana", "cherry"}y = {"google", "runoob", "apple"}
# 判断集合 y 中是否包含集合 x 中的元素,如果没有返回 True, 有则返回 False
z = x.isdisjoint(y)
# 结果返回 False,说明集合 y 中有和 x 中相同的元素print(z)

x = {"apple", "banana", "cherry"}y = {"google", "runoob", "baidu"}

# 判断集合 y 中是否包含集合 x 中的元素,如果没有返回 True, 有则返回 False
z = x.isdisjoint(y)

# 结果返回 True,说明集合 y 中没有和 x 中相同的元素print(z)

输出结果:FalseTrue

4.7 issubset()

issubset() 方法用于判断集合的所有元素是否都包含在指定集合中,如果是则返回 True,否则返回 False。

语法:

issubset() 
方法语法:
set.issubset(set)
**参数**set -- 必需,要比查找的集合返回值返回布尔值,如果都包含返回 True,否则返回 False

实例:

# 判断集合 x 的所有元素是否都包含在集合 y 中:
x = {"a", "b", "c"}y = {"f", "e", "d", "c", "b", "a"} z = x.issubset(y)  print(z)
输出结果# 说明 集合 x 中的元素都包含在 y 中True

注意:必须是集合中的元素都包含在内,否则结果为false

# 集合 y 中只有元素 b 和 c ,执行结果为False 
x = {"a", "b", "c"}y = {"f", "e", "d", "c", "b","y"}
z = x.issubset(y)
print(z)
结果输出;False

4.8 issuperset()

issuperset() 方法用于判断指定集合的所有元素是否都包含在原始的集合中,如果是则返回 True,否则返回 False。

语法:

set.issuperset(set)

实例:

# 判断集合 y 的所有元素是否都包含在集合 x 中:
x = {"f", "e", "d", "c", "b", "a"}y = {"a", "b", "c"} z = x.issuperset(y)  print(z)输出结果为:
True

# 如果没有全部包含返回 False:
实例 
1x = {"f", "e", "d", "c", "b"}y = {"a", "b", "c"} z = x.issuperset(y)  print(z)输出结果为:
False

4.9 symmetric_difference()

symmetric_difference() 方法返回两个集合中不重复的元素集合,即会移除两个集合中都存在的元素,结果返回一个新的集合。

语法:

set.symmetric_difference(set)

实例:

# 返回两个集合组成的新集合,但会移除两个集合的重复元素:
x = {"apple", "banana", "cherry"}y = {"google", "runoob", "apple"}
z = x.symmetric_difference(y)
print(z)
输出结果:{'banana', 'google', 'cherry', 'runoob'}

4.10 symmetric_difference_update()

symmetric_difference_update() 方法移除当前集合中在另外一个指定集合相同的元素,并将另外一个指定集合中不同的元素插入到当前集合中。

语法:

set.symmetric_difference_update(set)

实例:

# 在原始集合 x 中移除与 y 集合中的重复元素,并将不重复的元素插入到集合 x 中:
x = {"apple", "banana", "cherry"}y = {"google", "runoob", "apple"}
x.symmetric_difference_update(y)
print(x)
输出结果:{'runoob', 'cherry', 'banana', 'google'}

其他几个方法是对集合的增删改查,如:add() clear() copy() update() pop() remove() discard() 等方法,这些方法在对集合的基本操作章节有详解,大家到时候按需使用。

Python 函数的参数

定义一个函数非常简单,但是怎么定义一个函数,需要什么参数,怎么去调用却是我们需要去思考的问题。

如同大多数语言一样(如 Java),Python 也提供了多种参数的设定(如:默认值参数、关键字参数、形参等)。使用这些参数定义出来的代码,可以让我们适应不同的开放场景,也能简化我们的代码开发工作。

默认值参数

我们创建一个函数,定义参数中一个或多个赋予默认值后,我们可以使用比允许的更少的参数去调用此函数,举个例子(注意:以下代码都使用python3.7版本):

def def_param_fun(prompt, retries=4, reminder='Please try again!'):    
	while True:        
		ok = input(prompt)        
		if ok in ('y', 'ye', 'yes'):            
			return True        
		if ok in ('n', 'no', 'nop', 'nope'):            
			return False        
			retries = retries - 1        		
		if retries < 0:            
			raise ValueError('invalid user response')        
		print(reminder)        
# 我们可以如下进行调用
def_param_fun('Do you really want to quit?')
def_param_fun('Do you really want to quit?', 2)
def_param_fun('Do you really want to quit?', 2, 'Please, yes or no!')

如上所示,我们可以使用一个或多个参数去调用此函数,我们实际生产中,很多情况下会赋予函数参数默认值的情形,因此,合理使用此种参数形式可以简化我们很多工作量。

重要:使用默认值参数时,如果我们的默认值是一个可变对象时,我们调用函数可能出现不符合我们预期的结果。如下:

def f(a, l=[]):    
	l.append(a)    
    return l    
# 此时调用函数
print(f(1))
print(f(2))
print(f(3))
# 返回值
# [1]
# [1, 2]
# [1, 2, 3]

这是由于函数在初始化时,默认值只会执行一次,所以在默认值为可变对象(列表、字典以及大多数类实例),我们可以如下操作:

def f(a, l=None):    
	if l is None:        
		l = []    
	l.append(a)    
	return l
# 再次调用函数
print(f(1))
print(f(2))
print(f(3))
# 返回值
# [1]
# [2]
# [3]

可变参数

可变参数也就是我们对于函数中定义的参数是可以一个或多个可以变化的,其中 *args代表着可以传入一个list或者tuple, **args代表着可以传入一个dict。举个例子:

def variable_fun(kind, *arguments, **keywords):    
	print("friend : ", kind, ";")    
	print("-" * 40)    
	for arg in arguments:        
		print(arg)    
	print("-" * 40)    
	for kw in keywords:        
		print(kw, ":", keywords[kw])       
# 函数调用
variable_fun("xiaoming",             "hello xiaoming", "nice to meet you!",            
mother="xiaoma",            father="xiaoba",           
son="see you")           
# 输出结果
first arg:  xiaoming ...----------------------------------------
hello 
nice to meet you!----------------------------------------
mother : xiaoma
father : xiaoba
son : see you

我们还可以使用下面的方式进行调用,得到上面相同的结果:

list01 = ["hello xiaoming", "nice to meet you!"]
dict01 = {'mother': 'xiaoma', 'father': 'xiaoba', 'son': 'see you'}
variable_fun("xiaoming", *list01, **dict01)

以上其实是python的解包操作,和java类似。

关键字参数

关键字参数允许你调用函数时传入0个或任意个含参数名的参数,这样可以让我们灵活的去进行参数的调用。举个例子:

# 借用官网例子
def key_fun(voltage, state='a stiff', action='voom', type='Norwegian Blue'):    	print("-- This key_fun wouldn't", action, end=' ')    
	print("if you put", voltage, "volts through it.")    
	print("-- Lovely plumage, the", type)    
	print("-- It's", state, "!")
# 函数调用  
key_fun(1000)                                          # 1 positional argumentkey_fun(voltage=1000)                                  # 1 keyword argumentkey_fun(voltage=1000000, action='VOOOOOM')             # 2 keyword argumentskey_fun(action='VOOOOOM', voltage=1000000)             # 2 keyword argumentskey_fun('a million', 'bereft of life', 'jump')         # 3 positional argumentskey_fun('a thousand', state='pushing up the daisies')  # 1 positional, 1 keyword

注意不可以重复传值,否则会报如下错误:

# TypeError: key_fun() got multiple values for argument 'voltage'key_fun(100, voltage=1000)                             # err

Python 高阶函数

函数式编程现在逐渐被广大开发群体接受,越来越多的开发者们开始使用这种优雅的开发模式,而我们使用函数式编程最主要的是需要清楚:

  1. 什么是高阶函数(Higher-order Functions)?
  2. Python 中高阶函数有哪些?要怎么用?

高阶函数概念

在函数式编程中,我们可以将函数当作变量一样自由使用。一个函数接收另一个函数作为参数,这种函数称之为高阶函数。

举个例子:

def high_func(f, arr):    
    return [f(x) for x in arr]

上面的例子中, high_func 就是一个高阶函数。其中第一个参数 f 是一个函数,第二个参数 arr 是一个数组,返回的值是数组中的所有的值在经过 f 函数计算后得到的一个列表。例如:

from math import factorial
def high_func(f, arr):    
    return [f(x) for x in arr]
def square(n):   
    return n ** 2
# 使用python自带数学函数
print(high_func(factorial, list(range(10))))
# print out: [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880]
# 使用自定义函数
print(high_func(square, list(range(10))))
# print out: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Python 常用高阶函数

如同java、scala等语言,我们很多常用的高阶函数基本都一致。在开发中我们经常使用的最基本的高阶函数其实就几个,而我们也可以基于这些函数去进行适当的扩展,那么下面开始介绍几种常用的高阶函数。

map

Make an iterator that computes the function using arguments from each of the iterables. Stops when the shortest iterable is exhausted.

根据提供的函数对指定序列做映射, 并返回映射后的序列,定义:

map(func, *iterables) --> map object
  • function # 序列中的每个元素需要执行的操作, 可以是匿名函数
  • *iterables # 一个或多个序列

正如前面所举的例子 high_func 函数, map 函数是 high_func 函数高阶版,可以传入一个函数和多个序列。

from math import factorial
def square(n):    
    return n ** 2
# 使用python自带数学函数
facMap = map(factorial, list(range(10)))print(list(facMap))
# print out: [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880]
# 使用自定义函数
squareMap = map(square, list(range(10)))print(list(squareMap))
# print out: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

可以看到输出了同样的结果,只是与 python2.X 不用的是, python3.X 中返回 map类 ,而前者直接返回一个列表。

我们使用匿名函数,也可以传入多个序列,如下

# 使用匿名函数
lamMap = map(lambda x: x * 2, list(range(10)))print(list(lamMap))
# print out: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# 传入多个序列
mutiMap = map(lambda x, y: x+y, list(range(10)), list(range(11, 15)))print(list(mutiMap))
# print out: [11, 13, 15, 17]

reduce

Apply a function of two arguments cumulatively to the items of a sequence,from left to right, so as to reduce the sequence to a single value.

大致上来讲, reduce 函数需要传入一个有两个参数的函数,然后用这个函数从左至右顺序遍历序列并生成结果,定义如下:

reduce(function, sequence[, initial]) -> value
  • function # 函数, 序列中的每个元素需要执行的操作, 可以是匿名函数
  • sequence # 需要执行操作的序列
  • initial # 可选,初始参数

最后返回函数的计算结果, 和初始参数类型相同

简单举个例子:

# 注意,现在 reduce() 函数已经放入到functools包中。
from functools import reduce
result = reduce(lambda x, y: x + y, [1, 2, 3, 4, 5])
print(result)
# print out 15

我们可以看到,序列 [1, 2, 3, 4, 5] 通过匿名函数进行了累加。

设定初始值:

# 设定初始参数:
s = reduce(lambda x, y: x + y, ['1', '2', '3', '4', '5'], "数字 = ")
print(s)
# print out:数字 = 12345

需要注意的是:序列数据类型需要和初始参数一致。

filter

Return an iterator yielding those items of iterable for which function(item) is true. If function is None, return the items that are true.

filter() 函数用来过滤序列中不符合条件的值,返回一个迭代器,该迭代器生成那些函数(项)为true的iterable项。如果函数为None,则返回为true的项。定义如下:

filter(function or None, iterable) --> filter object
  • function or None # 过滤操作执行的函数
  • iterable # 需要过滤的序列

举个例子:

def boy(n):   
    if n % 2 == 0:        
        return True    
    return False
# 自定义函数
filterList = filter(boy, list(range(20)))
print(list(filterList))
# print out: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# 自定义函数
filterList2 = filter(lambda n: n % 2 == 0, list(range(20)))
print(list(filterList2))
# print out: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

上面我们可以看到,列表中不能被 2 整除的数据都被排除了。

sorted

Return a new list containing all items from the iterable in ascending order.

A custom key function can be supplied to customize the sort order, and the reverse flag can be set to request the result in descending order.

sorted 函数默认将序列升序排列后返回一个新的 list,还可以自定义键函数来进行排序,也可以设置 reverse 参数确定是升序还是降序,如果 reverse = True 则为降序。函数定义如下:

def sorted(iterable: Iterable[_T], *,           
           key: Optional[Callable[[_T], Any]] = ...,           
           reverse: bool = ...) -> List[_T]: ...
  • iterable # 序列
  • key # 可以用来计算的排序函数。
  • reverse # 排序规则,reverse = True降序,reverse = False 升序(默认)。

举个简单例子:

list01 = [5, -1, 3, 6, -7, 8, -11, 2]
list02 = ['apple', 'pig', 'monkey', 'money']
print(sorted(list01))
# print out: [-11, -7, -1, 2, 3, 5, 6, 8]
print(sorted(list01, key=abs))
# print out: [-1, 2, 3, 5, 6, -7, 8, -11]
# 默认升序
print(sorted(list02))
# print out: ['apple', 'money', 'monkey', 'pig']
# 降序
print(sorted(list02, reverse=True))
# print out: ['pig', 'monkey', 'money', 'apple']

# 匿名函数排序
print(sorted(list02, key=lambda x: len(x), reverse=True))
# print out: ['monkey', 'apple', 'money', 'pig']

Python 输入输出

1 格式化输出

Python 输出值的方式有两种:表达式语句和 print 函数(文件对象的输出使用 write 方法,标准文件输出可以参考 sys.stdout ,详细文档:https://docs.python.org/zh-cn/3/faq/extending.html#how-do-i-catch-the-output-from-pyerr-print-or-anything-that-prints-to-stdout-stderr)。

如果我们想要将输出的值转成字符串,可以使用 repr() 或 str() 函数来实现,其中 repr() 函数产生一个解释器易读的表达形式,str() 函数返回一个用户易读的表达形式。

如果我们不只是想打印使用空格分隔的值,而是想对输出进行格式化控制,可以采用两种方式:一种是自己处理整个字符串,另一种是采用 str.format() 方式,下面介绍下 str.format() 的使用。

1)基本使用

print('{}网址:"{}!"'.format('Python技术', 'www.justdopython.com'))
Python技术网址:"www.justdopython.com!"

括号及其里面的字符 (称作格式化字段) 将会被 format() 中的参数替换

2)在括号中的数字用于指向传入对象在 format() 中的位置

print('{0} 和 {1}'.format('Hello', 'Python'))
Hello 和 Python
print('{0} {1}'.format('Hello', 'Python'))
Hello Python
print('{1} {0}'.format('Hello', 'Python'))
Python Hello

3)如果在 format() 中使用了关键字参数,那么它们的值会指向使用该名字的参数

print('{name}网址:{site}'.format(name='Python技术', site='www.justdopython.com'))
Python技术网址:www.justdopython.com

4)位置及关键字参数可以任意的结合

print('电商网站 {0}, {1}, {other}。'.format('淘宝', '京东', other='拼多多'))
电商网站 淘宝, 京东, 拼多多。

5)用 ** 标志将字典以关键字参数的方式传入

"repr() shows quotes: {!a}; str() doesn't: {!s}".format('test1', 'test2')
"repr() shows quotes: 'test1'; str() doesn't: test2"

6)字段名后允许可选的 : 和格式指令

# 将 PI 转为三位精度
import math
print('The value of PI is approximately {0:.3f}.'.format(math.pi))
The value of PI is approximately 3.142.

7)在字段后的 : 后面加一个整数会限定该字段的最小宽度

table = {'Sjoerd': 123, 'Jack': 456, 'Dcab': 789}
for name, phone in table.items():print('{0:10} ==> {1:10d}'.format(name, phone))
Jack       ==>       456
Dcab       ==>       789
Sjoerd     ==>       123

8)如果有个很长的格式化字符串,不想分割它可以传入一个字典,用中括号( [] )访问它的键;

table = {'Sjoerd': 123, 'Jack': 456, 'Dcab': 789789789789}
print('Jack: {0[Jack]:d}; Sjoerd: {0[Sjoerd]:d}; ' 'Dcab: {0[Dcab]:d}'.format(table))
Jack: 456; Sjoerd: 123; Dcab: 789789789789

还可以用 ** 标志将这个字典以关键字参数的方式传入。

table = {'Sjoerd': 123, 'Jack': 456, 'Dcab': 789789789789}
print('Jack: {Jack:d}; Sjoerd: {Sjoerd:d}; Dcab: {Dcab:d}'.format(**table))
Jack: 456; Sjoerd: 123; Dcab: 789789789789

2 读取键盘输入

Python 提供了 input() 内置函数从标准输入读入一行文本,默认的标准输入是键盘,input() 可以接收一个 Python 表达式作为输入,并将运算结果返回。示例如下:

str = input("请输入:");
print ("输入的内容是: ", str)
请输入:Hello Python
你输入的内容是:  Hello Python

3 文件读写

函数 open() 返回文件对象,通常的用法需要两个参数:open(filename, mode)。

第一个参数 filename 是要访问的文件名,第二个参数 mode 是描述如何使用该文件(可取值主要包括:‘r’ 读取文件;‘w’ 只是写入文件,已经存在的同名文件将被删掉;‘a’ 打开文件进行追加,自动添加到末尾;‘r+’ 打开文件进行读取和写入;‘rb+’ 以二进制格式打开一个文件用于读写…),mode 参数是可选的,默认为 ‘r’。

3.1 文件对象方法

  • read()

要读取文件内容,调用 read(size) ,size为可选参数。

f = open('tmp.txt', 'r')
str = f.read(5)
print(str)
f.close()
Hello
  • readline()

读取一行,换行符为 \n 。

f = open('tmp.txt', 'r')
str = f.readline()
print(str)
f.close()
  • readlines()

读取文件中包含的所有行,可设置可选参数 size 。

f = open('tmp.txt', 'r')
str = f.readlines(1)
print(str)
f.close()['Hello Python']
  • write()

write(string) 将 string 的内容写入文件。

f = open('tmp.txt', 'w')
num = f.write('Hello Python')
print(num)
f.close()
12
  • seek()

seek(offset, from_what) 改变文件当前的位置。offset 移动距离;from_what 起始位置,0 表示开头,1 表示当前位置,2 表示结尾,默认值为 0 ,即开头。

f = open('tmp.txt', 'rb+')
f.write(b'0123456789abcdef')
# 移动到文件的第 6 个字节
f.seek(5)
print(f.read())
b'56789abcdef'
  • tell()

tell() 返回文件对象当前所处的位置,它是从文件开头开始算起的字节数。

f = open('tmp.txt', 'r')
f.seek(5)
print(f.tell())
5
  • close()

当你处理完一个文件后,调用 close() 来关闭文件并释放系统的资源。也可以使用 with 关键字处理文件对象,实现文件用完后自动关闭。

with open('tmp.txt', 'r') as f: 
    read_data = f.read()
print(f.closed)
True

3.2 操作 json 格式数据

  • json.dumps(obj) 序列化,obj 转换为 json 格式的字符串;
  • json.dump(obj, fp) 序列化,将 obj 转换为 json 格式的字符串,将字符串写入文件;
  • json.loads(str) 反序列化,将 json 格式的字符串反序列化为一个 Python 对象;
  • json.load(fp) 反序列化,从文件中读取含 json 格式的数据,将之反序列化为一个 Python 对象。
import json
data = {'id':'1', 'name':'jhon', 'age':12}
with open('t.json', 'w') as f:
    json.dump(data, f)
with open("t.json", 'r') as f:
    d = json.load( f)
print(d){'id': '1', 'name': 'jhon', 'age': 12}

Python 错误和异常

作为 Python 初学者,在刚学习 Python 编程时,经常会看到一些报错信息,这些报错信息就是我们接下来要讲的错误和异常。

我们在执行程序语句的时候,经常会看到命令行输出报错信息,例如:

while True print('Hello world')  
	File "<stdin>", line 1, in ?   
		while True print('Hello world')
        				^
SyntaxError: invalid syntax

这种报错信息会阻止程序正常运行,也就是我们要介绍的错误和异常。

错误

我们说的错误指的是Python的语法错误,例如:

if 1=1: print('always')  
	File "<stdin>", line 1   
		if 1=1: print('always')  
        	^
SyntaxError: invalid syntax

上面例子中,在判断相等的时候应该用’‘==’,而不是用’=',执行的时候,语法解析器检查到有错误,程序语句终止执行,并将错误的地方用上箭头指出来。

语法错误很好解决,根据命令行提示的错误位置,检查语法,改正即可。

异常

在Python中,即使你的代码没有语法错误,也不能保证程序按照你的想法运行完毕,因为在程序执行过程中也会有错误。程序运行期间检测到的错误被称为异常,例如:

'1' + 2
Traceback (most recent call last):  
    File "<stdin>", line 1, in ?
TypeError: Can't convert 'int' object to str implicitly

大多数的异常都不会被程序处理,都以错误信息的形式显示出来,如上例所示,提示信息告诉我们int类型不能和str类型相加。

错误提示信息会告诉我们异常发生的上下文,并以调用栈的形式显示具体信息,提示信息的最后一行开头会显示错误类型名称,上例中,错误类型为’TypeError’,表示类型异常。

什么是异常

异常是一个事件,该事件会在程序执行过程中发生,从而影响程序的正常执行。当 Python遇到无法处理的程序时,就会引发一个异常。在 Python 中,异常是一个对象,用于表示一个错误,当 Python脚本发生异常时我们需要捕获和处理它,否则程序会终止执行。

处理异常

Python 提供了 try/except语句用来捕获和处理异常。try 语句用来检测语句块中是否有错误,except 语句则用来捕获 try 语句中的异常,并进行处理,附加的 else 可以在 try 语句没有异常时执行。

语法

下面以最简单的 try…except…else 为例:

try:  
    statement(s)            # 要检测的语句块
except exception:   
	deal_exception_code # 如果在 try 部份引发了 'exception' 异常
except exception2, e:    			
    deal_exception2_code # 如果引发了 'exception2' 异常
else:    
    no_exception_happend_code #如果没有异常发生

try 语句的执行逻辑如下:

  • 首先,执行 try 子句 (try 和 except 关键字之间的(多行)语句)。
  • 如果没有异常发生,则跳过 except 子句 并完成 try 语句的执行。
  • 如果在执行try 子句时发生了异常,则跳过该子句中剩下的部分。然后,如果异常的类型和 except 关键字后面的异常匹配,则执行 except 子句,然后继续执行 try 语句之后的代码。
  • 如果发生的异常和 except 子句中指定的异常不匹配,则将其传递到外部的 try 语句中;如果没有找到处理程序,则它是一个 未处理异常,执行将停止并显示错误消息。
  • 如果 try 语句执行时没有发生异常,那么将执行 else 语句后的语句(如果有 else 的话),然后控制流通过整个 try 语句。
基类

如果发生的异常和 except 子句中的类是同一个类或者是它的基类,则异常和 except 子句中的类是兼容的(但反过来则不成立 — 列出派生类的 except 子句与基类兼容)。

实例
class BException(Exception):  #继承Exception基类   
    pass
class CException(BException):  #继承BException基类   
    pass
class DException(CException):  #继承CException基类   
    pass
for cls in [BException, CException, DException]:    
    try:        
        raise cls()  #抛出异常   
    except DException:       
        print("D")    
    except CException:        
        print("C")   
    except BException:      
        print("B")
#输出
B
C
D

请注意如果 except 子句被颠倒(把 except BException 放到第一个),它将打印 B,B,B — 因为DException类继承CException类,CException类继承BException类,将 except BException 放到第一个可以匹配这三个异常,后面的 except 就不会执行。

不带异常类型的 except

Python可以在所有 except 的最后加上 except 子句,这个子句可以省略异常名,以用作通配符。它可以捕获前面任何 except (如果有的话)没有捕获的所有异常。

try:    
    statement(s)            # 要检测的语句块
except exception:   
	deal_exception_code # 如果在 try 部份引发了 'exception' 异常
except :    
    deal_all_other_exception2_code # 处理全部其它异常
else:   
    no_exception_happend_code #如果没有异常发生
实例
try:    
    raise BException()  #抛出异常
except DException:    
    print("D")
except:    
    print("处理全部其它异常") #处理全部其它异常
#输出
处理全部其它异常
except 语句捕获多种异常类型

一个 try 语句可能有多个 except 子句,以指定不同异常的处理程序,最多会执行一个处理程序。处理程序只处理相应的 try 子句中发生的异常,而不处理同一 try 语句内其他处理程序中的异常。一个 except 子句可以将多个异常命名为带括号的元组。

try:    
    statement(s)            # 要检测的语句块
except exception:    
	deal_exception_code # 如果在 try 部份引发了 'exception' 异常
except (Exception1[, Exception2[,...ExceptionN]]]) :    
    deal_all_other_exception2_code # 处理多个异常
else:   
    no_exception_happend_code #如果没有异常发生
实例
try:    
    raise BException()  #抛出异常
except (BException, DException):    
    print("D")
except:    
    print("处理全部其它异常") #处理全部其它异常
else:    
    print("没有异常发生") #没有异常发生
#输出
D
try - finally 语句

finally 语句用于无论是否发生异常都将执行最后的代码。

try:   
    # <语句>
finally:   
    # <语句>    #退出try时总会执行
实例
try:   
    raise BException()  #抛出异常
except (BException, DException):    
    print("D")
except:    
    print("处理全部其它异常") #处理全部其它异常
else:    
    print("没有异常发生") #没有异常发生
finally:    
    print("你们绕不过我,必须执行") #必须执行的代码    
#输出
D
你们绕不过我,必须执行

这里注意 finally 和 else 的区别,finally 是无论是否有异常都会执行,而 else 语句只有没有异常时才会执行。也就是说如果没有异常,那么 finally 和 else 都会执行。

异常的参数

except 子句可以在异常名称后面指定一个变量。这个变量和一个异常实例绑定,它的参数是一个元组,通常包含错误字符串,错误数字,错误位置,存储在 .args 中。为了方便起见,异常实例定义了__str__() ,因此可以直接打印参数而无需引用 .args。

try:   
    # 正常的操作 ......
except ExceptionType as inst:    
    # 可以在这输出 inst 的值.....
实例
try:    
    x = 1 / 0  # 除数为0
except ZeroDivisionError as err: #为异常指定变量err   
    print("Exception")    
    print(err.args) #打印异常的参数元组    
    print(err) #打印参数,因为定义了__str__()
#输出
Exception('division by zero',)
division by zero
触发异常

Python 提供了 raise 语句用于手动引发一个异常。

语法
raise [Exception [, args [, traceback]]]
参数说明
Exception:异常的类型,例如 ZeroDivisionError
args:异常参数值,可选,默认值 "None"
traceback:可选,用于设置是否跟踪异常对象

异常参数值可以是一个字符串,类或对象

实例
def diyException(level):    
    if level > 0:        
        raise Exception("raise exception", level)  #主动抛出一个异常,并且带有参数        
        print('我是不会执行的') #这行代码不会执行
try:   
    diyException(2)  #执行异常方法
except Exception as err: #捕获异常    
    print(err) #打印异常参数    
#输出
('raise exception', 2)

为了能够捕获异常,"except"语句必须有用相同的异常来抛出类对象或者字符串。如果要捕获上面代码抛出的异常,except 语句应该如下所示:

#定义函数
def diyException(level):    
    if level > 0:        
        raise Exception("error level", level)  #主动抛出一个异常,并且带有参数        
        print('我是不会执行的') #这行代码不会执行
try:   
    diyException(2)  #执行异常方法
except 'error level' as err: #捕获异常    
    print(err) #打印异常参数
#输出
Traceback (most recent call last):  
    File "/Users/cxhuan/Documents/python_workspace/stock/test.py", line 51, in <module>    
    diyException(2)  #执行异常方法  
    File "/Users/cxhuan/Documents/python_workspace/stock/test.py", line 47, in diyException    raise Exception("error level", level)  #主动抛出一个异常,并且带有参数
Exception: ('error level', 2)

当然,我们也可以通过 traceback 来捕获异常:

import traceback
#定义函数
def diyException(level):    
    if level > 0:       
        raise Exception("error level", level)  #主动抛出一个异常,并且带有参数        
        print('我是不会执行的') #这行代码不会执行
try:   
    diyException(2)  #执行异常方法
except Exception: #捕获异常    
    traceback.print_exc()
#输出
Traceback (most recent call last): 
    File "/Users/cxhuan/Documents/python_workspace/stock/test.py", line 51, in <module>    
    diyException(2)  #执行异常方法  
    File "/Users/cxhuan/Documents/python_workspace/stock/test.py", line 47, in diyException    
    raise Exception("error level", level)  #主动抛出一个异常,并且带有参数
Exception: ('error level', 2)
用户自定义异常

除了使用 Python 内置的异常,我们还可以创建自己的异常类型。创建自己的异常非常简单,只需要创建一个类,并继承 Exception 类或其子类。

下面的代码创建了一个异常 DiyError 继承自 Python 内置的 RuntimeError,用于在异常触发时输出更多的信息。

#自定义异常
class DiyError(RuntimeError):    
    def __init__(self, arg):        
        self.args = arg
try:    
    raise DiyError("my diy exception") #触发异常
except DiyError as e:   
    print(e)

定义好了之后,我们就可以在 except 语句后使用 DiyError 异常,变量 e 是用于创建 DiyError 类的实例。我们也可以通过 raise 语句手动触发这个异常。

预定义的清理行为

一些对象定义了标准的清理行为,无论系统是否成功的使用了它,一旦不需要它了,那么这个标准的清理行为就会执行。

for line in open("myfile.txt"):   
    print(line, end="")

上面这个例子尝试打开一个文件,然后把内容打印出来。但是有一个问题:当执行完毕后,程序没有关闭文件流,文件会保持打开状态。

关键词 with 语句就可以保证诸如文件之类的对象在使用完之后一定会正确的执行他的清理方法。

with open("myfile.txt") as f:    
    for line in f:        
        print(line, end="")

以上这段代码执行完毕后,就算在处理过程中出问题了,文件 f 总是会关闭。这里面的原理就是使用了 finally 机制,有兴趣的可以去深入了解一下。

Python 之引用

1. 引用简介与工具引入

Python 中对于变量的处理与 C 语言有着很大的不同,Python 中的变量具有一个特殊的属性:identity,即“身份标识”。这种特殊的属性也在很多地方被称为“引用”。

为了更加清晰地说明引用相关的问题,我们首先要介绍两个工具:一个Python的内置函数:id();一个运算符:is;同时还要介绍一个sys模块内的函数:getrefcount()

1.1 内置函数id()

id(object)

Return the “identity” of an object. This is an integer which is guaranteed to be unique and constant for this object during its lifetime. Two objects with non-overlapping lifetimes may have the same id()[1] value.

返回值为传入对象的“标识”。该标识是一个唯一的常数,在传入对象的生命周期内与之一一对应。生命周期没有重合的两个对象可能拥有相同的id()返回值。

CPython implementation detail: This is the address of the object in memory.

CPython 实现细节:“标识”实际上就是对象在内存中的地址。

——引自《Python 3.7.4 文档-内置函数-id()[2]》

换句话说,不论是否是 CPython 实现,一个对象的id就可以视作是其虚拟的内存地址。

1.2 运算符is

运算含义
isobject identity

is的作用是比较对象的标识。

——引自《Python 3.7.4 文档-内置类型[3]》

1.3 sys模块函数getrefcount()函数

sys.getrefcount(object)

Return the reference count of the object. The count returned is generally one higher than you might expect, because it includes the (temporary) reference as an argument to getrefcount()[4].

返回值是传入对象的引用计数。由于作为参数传入getrefcount()的时候产生了一次临时引用,因此返回的计数值一般要比预期多1。

——引自《Python 3.7.4 文档-sys模块——系统相关参数及函数[5]》

此处的“引用计数”,在 Python 文档[6]中被定义为“对象被引用的次数”。一旦引用计数归零,则对象所在的内存被释放。这是 Python 内部进行自动内存管理的一个机制。

2. 问题示例

C 语言中,变量代表的就是一段固定的内存,而赋给变量的值则是存在这段地址中的数据;但对 Python 来说,变量就不再是一段固定的地址,而只是 Python 中各个对象所附着的标签。理解这一点对于理解 Python 的很多特性十分重要。

2.1 对同一变量赋值

举例来说,对于如下的 C 代码:

int a = 10000;
printf("original address: %p\n", &a); // original address: 0060FEFC
a = 12345;
printf("second address: %p\n", &a); // second address: 0060FEFC

对于有 C 语言编程经验的人来说,上述结果是显而易见的:变量a的地址并不会因为赋给它的值有变化而发生变化。对于 C 编译器来说,变量a只是协助它区别各个内存地址的标识,是直接与特定的内存地址绑定的,如图所示:

图片

但 Python 就不一样的。考虑如下代码:

a = 10000
id(a)
1823863879824
a = 12345
id(a)
1823863880176

这就有点儿意思了,更加神奇的是,即使赋给变量同一个常数,其得到的id也可能不同:

a = 10000
id(a)
1823863880304
a = 10000
id(a)
1823863879408

假如a对应的数据类型是一个列表,那么:

a = [1,2]
id(a)
2161457994952
a = [1,2]
id(a)
2161458037448

得到的id值也是不同的。

正如前文所述,在 Python 中,变量就是一块砖,哪里需要哪里搬。每次将一个新的对象赋值给一个变量,都在内存中重新创建了一个对象,这个对象就具有新的引用值。作为一个“标签”,变量也是哪里需要哪里贴,毫无节操可言。

图片

但要注意的是,这里还有一个问题:之所以说“即使赋给变量同一个常数,其得到的id可能不同”,实际上是因为并不是对所有的常数都存在这种情况。以常数1为例,就有如下结果:

a = 1
id(a)140734357607232
a = 1
id(a)140734357607232
id(1)140734357607232

可以看到,常数1对应的id一直都是相同的,没有发生变化,因此变量aid也就没有变化。

这是因为Python在内存中维护了一个特定数量的常量池,对于一定范围内的数值均不再创建新的对象,而直接在这个常量池中进行分配。实际上在我的机器上使用如下代码可以得到这个常量池的范围是 [0, 256] ,而 256 刚好是一个字节的二进制码可以表示的值的个数。

for b in range(300):    
    if b is not range(300)[b]:        
        print("常量池最大值为:", (b - 1))       
        break# 常量池最大值为:256

相应地,对于数值进行加减乘除并将结果赋给原来的变量,都会改变变量对应的引用值:

a = 10000
id(a)
2161457772304
a = a + 1
a
10001
id(a)
2161457772880

比较代码块第 3、8行的输出结果,可以看到对数值型变量执行加法并赋值会改变对应变量的引用值。这样的表现应该比较好理解。因为按照 Python 运算符的优先级,a = a + 1实际上就是a = (a + 1),对变量a对应的数值加1之后得到的是一个新的数值,再将这个新的数值赋给a ,于是a的引用也就随之改变。列表也一样:

a = [1,2]
id(a)
2161458326920
a = a + [4]
a
[1, 2, 4]
id(a)
2161458342792

2.2 不变的情况

与数值不同,Python 中对列表对象的操作还表现出另一种特性。考虑下面的代码:

c = [1, 2, 3]
id(c)
2161458355400
c[2] = 5
c
[1, 2, 5]
id(c)
2161458355400
c.append(3)
c
[1, 2, 5, 3]
id(c)
2161458355400

观察代码块第 3、8、13三行,输出相同。也就是说,对于列表而言,可以通过直接操作变量本身,从而在不改变其引用的情况下改变所引用的值。

更进一步地,如果是两个变量同时引用同一个列表,则对其中一个变量本身直接进行操作,也会影响到另一个变量的值:

c = [1, 2, 3]
cc = c
id(c)
1823864610120
id(cc)
1823864610120

显然此时的变量cccid是一致的。现在改变c所引用的列表值:

c[2] = 5
cc
[1, 2, 5]

可以看到cc所引用的列表值也随之变化了。再看看相应地id

id(c)
1823864610120
id(cc)
1823864610120

两个变量的id都没有发生变化。再调用append()方法:

c.append(3)
c
[1, 2, 5, 3]
cc
[1, 2, 5, 3]
id(c)
1823864610120
id(cc)
1823864610120

删除元素:


>>> del c[3]
>>> c
[1, 2, 5]
>>> cc
[1, 2, 5]
>>> id(c)
1823864610120
>>> id(cc)
1823864610120

在上述所有对列表的操作中,均没有改变相应元素的引用。

也就是说,对于变量本身进行的操作并不会创建新的对象,而是会直接改变原有对象的值。

2.3 一个特殊的地方

本小节示例灵感来自[关于Python中的引用[7]]

数值数据和列表还存在一个特殊的差异。考虑如下代码:

>>> num = 10000
>>> id(num)
2161457772336
>>> num += 1
>>> id(num)
2161457774512

有了前面的铺垫,这样的结果很显得很自然。显然在对变量num进行增1操作的时候,还是计算出新值然后进行赋值操作,因此引用发生了变化。

但列表却不然。见如下代码:

>>> li = [1, 2, 3]
>>> id(li)
2161458469960
>>> li += [4]
>>> id(li)
2161458469960
>>> li
[1, 2, 3, 4]

注意第 4 行。明明进行的是“相加再赋值”操作,为什么有了跟前面不一样的结果呢?检查变量li的值,发现变量的值也确实发生了改变,但引用却没有变。

实际上这是因为加法运算符在 Python 中存在重载的情况,对列表对象和数值对象来说,加法运算的底层实现是完全不同的,在简单的加法中,列表的运算还是创建了一个新的列表对象;但在简写的加法运算+=实现中,则并没有创建新的列表对象。这一点要十分注意。

3. 原理解析

前面(第3天:Python 变量与数据类型[8])我们提到过,Python 中的六个标准数据类型实际上分为两大类:可变数据不可变数据。其中,列表、字典和集合均为“可变对象”;而数字、字符串和元组均为“不可变对象”。实际上上面演示的数值数据(即数字)和列表之间的差异正是这两种不同的数据类型导致的。

由于数字是不可变对象,我们不能够对数值本身进行任何可以改变数据值的操作。因此在 Python 中,每出现一个数值都意味着需要另外分配一个新的内存空间(常量池中的数值例外)。

>>> a = 10000
>>> a == 10000
True
>>> a is 10000
False
>>> id(a)
2161457773424
>>> id(10000)
2161457773136
>>> 
from sys import getrefcount
>>> getrefcount(a)
2
>>> getrefcount(10000)
3

前 9 行的代码容易理解:即使是同样的数值,也可能具有不同的引用值。关键在于这个值是否来自于同一个对象。

而第 10 行的代码则说明除了getrefcount()函数的引用外,变量a所引用的对象就只有1个引用,也就是变量a。一旦变量a被释放,则相应的对象引用计数归零,也会被释放;并且只有此时,这个对象对应的内存空间才是真正的“被释放”。

而作为可变对象,列表的值是可以在不新建对象的情况下进行改变的,因此对列表对象本身直接进行操作,是可以达到“改变变量值而不改变引用”的目的的。

4. 总结

对于列表、字典和集合这些“可变对象”,通过对变量所引用对象本身进行操作,可以只改变变量的值而不改变变量的引用;但对于数字、字符串和元组这些“不可变对象”,由于对象本身是不能够进行变值操作的,因此要想改变相应变量的值,就必须要新建对象,再把新建对象赋值给变量。

Python 之迭代器

1 概念引入

在之前的教程中,我们已经接触过一些典型的for语句,比如:


>>> list_example = [0, 1, 2, 3, 4]
>>> for i in list_example:
...  print(i)
...
0
1
2
3
4

通过简单地使用forin两个关键字,我们可以很轻松地实现在 C 语言中繁琐的遍历操作。相比较而言,C 语言中要实现相同的功能,需要这样写(假设存在整型数组list_example):

int i;
for(i = 0; i < list_length; i++)
    printf("%d\n", list_example[i]);

显而易见,在遍历元素的操作上,Python 的表达更加直观优雅,简洁明了;这正是因为 Python 在实现for语句的时候,恰到好处地使用了“迭代器”的概念。

迭代器在 Python 中随处可见,并且具有统一的标准。通过使用迭代器,Python 能够逐个访问列表list_example中的每个元素。

下面我们来进一步讨论相关的机制。

2 定义及原理

2.1 迭代器的定义

迭代器(iterator)是一种可在容器(container)中遍访的接口,为使用者封装了内部逻辑。

——百度百科·迭代器 大意

上面是我们可以查到的、对“迭代器”的一个宽泛的定义。

而具体到 Python 中,迭代器也属于内置的标准类之一,是与我们之前学习过的“序列”同一层次的概念。

对于迭代器对象本身来说,需要具有**__iter__()[2]和__next__()**[3]两种方法,二者合称为“迭代器协议”。也就是说,只要同时具有这两种方法,Python 解释器就会认为该对象是一个迭代器;反之,只具有其中一个方法或者二者都不具有,解释器则认为该对象不是一个迭代器。

上述论断可由下面的代码验证(需要用到内置函数isinstance(),来判断一个对象是否是某个类的实例;该用法启发于[廖雪峰的官方网站[4]]):


>>> from collections import Iterable, Iterator, Container
>>> class bothIterAndNext:
... 	def __iter__(self):
... 		pass
... 	def __next__(self):
... 		pass
...
>>> isinstance(bothIterAndNext(), Iterable) # 两种方法都有的对象是可迭代的
True
>>> isinstance(bothIterAndNext(), Iterator) # 两种方法都有的对象是迭代器
True
>>> 
>>> class onlyNext:
... 	def __next__(self):
... 		pass
...
>>> isinstance(onlyNext(), Iterable) # 只有方法 __next__() 是不可迭代的
False
>>> isinstance(onlyNext(), Iterator) # 只有方法 __next__() 不是迭代器
False
>>> 
>>> class onlyIter:
... 	def __iter__(self):
... 		pass
...
>>> isinstance(onlyIter(), Iterable) # 只有方法 __iter__() 是可迭代的
True
>>> isinstance(onlyIter(), Iterator) # 只有方法 __iter__() 不是迭代器
False

由第 8~11 行的代码可知,对于 Python 来说,判断一个对象是否是迭代器的标准仅仅是“是否同时具有__iter__()__next__()这两个方法”。

并且从第 17~20 行的代码也可以验证上述推断:只具有方法__next__()既不是可迭代的,也不是一个迭代器。

有意思的事情发生在代码第 26、27 两行:代码输出结果显示,只有方法__iter__()的对象居然是可迭代的!(后文解释)

2.2 迭代器的实质

迭代器对象本质上代表的是一个数据流,通过反复调用其方法__next__()或将其作为参数传入next()函数,即可按顺序逐个返回数据流中的每一项;直到流中不再有数据项,从而抛出一个StopIteration异常,终止迭代。

在 Python 中内置了两个函数:iter()next(),分别用于“将参数对象转换为迭代器对象”和“从迭代器中取出下一项”。

实际上所有具有方法__iter__()的对象均被视作“可迭代的”。因为方法__iter__()进行的操作其实就是返回一个该对象对应的迭代器,也就是说“可迭代的(iterable)”的真实含义其实是“可以被转换为迭代器(iterator)的”。而内置函数iter()也是调用对象本身具有的__iter__()方法来实现特定对象到迭代器的转换。

相应地,内置函数next()其实是调用了对象本身的方法__next__(),而该方法执行的操作就是从对象对应的数据流中取出下一项。

因此直接调用对象的__iter__()__next__()方法与将对象作为参数传入内置函数iter()next()是等效的。

要注意的一点在于,对迭代器调用其本身的__iter__()方法,得到的将会是这个迭代器自身,该迭代器相关的状态都会被保留,包括该迭代器目前的迭代状态。见下述代码:

>>> li = [1, 2, 3]
>>> li_iterator = iter(li)
>>> isinstance(li, Iterator)
False
>>> isinstance(li_iterator, Iterator)
True

显然,列表li本身并不是一个迭代器,而将其传入内置函数iter()就得到了相应于列表li的迭代器li_iterator。我们调用next()函数来迭代它:

>>> next(li_iterator)
1
>>> next(li_iterator)
2

一切都在预料之中。我们再来将其本身作为参数传入内置函数iter()

>>> li_iterator = iter(li_iterator)
>>> next(li_iterator)
3

到这里跟我们希望的就有所出入了。在使用这样一个语句的时候,通常我们的目的都是得到一个新的迭代器,而非跟原先的迭代器一样的对象。

更进一步地,我们还可以发现,对迭代器调用iter()函数得到的对象不仅与原先的迭代器具有相同的状态,它们其实就是指向同一个对象

>>> id(li_iterator)
2195581916440
>>> li_iterator = iter(li_iterator)
>>> id(li_iterator)
2195581916440
>>> li_iterator2 = iter(li_iterator)
>>> id(li_iterator2)
2195581916440

也就是说在对象本身就是一个迭代器的情况下,生成的对应迭代器的时候 Python 不会进行另外的操作,就返回这个迭代器本身作为结果。

3 实现一个迭代器类

本节构建类的代码来自[Python3 文档-类-9.8 迭代器[5]]

有了上面的讨论,我们就可以自己实现一个简单的迭代器。只要确保这个简单迭代器具有与迭代器定义相符的行为即可。

说人话就是:要定义一个数据类型,具有__iter__()方法并且该方法返回一个带有__next__()方法的对象,而当该类已经具有__next__()方法时则返回其本身。示例代码如下:

class Reverse:
    """反向遍历序列对象的迭代器"""
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]

验证一下:

>>> rev = Reverse('justdopython.com')
>>> next(rev)
'm'
>>> next(rev)
'o'
>>> next(rev)
'c'
>>> next(rev)
'.'

4 for语句与迭代器

回到文章开头我们作为引子的for循环示例,实际上在执行for语句的时候,Python 悄悄调用了内置函数**iter()[6],并将for语句中的容器对象作为参数传入;而函数iter()**[7]返回值则是一个迭代器对象。

因此,for语句是将容器对象转换为迭代器对象之后,调用__next__()方法,逐个访问原容器中的各个对象,直到遍历完所有元素,抛出一个StopIteration异常,并终止for循环。

5 总结

  • 迭代器(iterator)首先要是可迭代的(iterable);即迭代器一定是可迭代的,但可迭代的不一定是迭代器
  • 可迭代的对象意味着可以被转换为迭代器
  • 迭代器需要同时具有方法__iter__()__next__()
  • 对迭代器调用iter()函数,得到的是这个迭代器本身
  • for循环实际上使用了迭代器,并且一般情况下将异常StopIteration作为循环终止条件

Python 之装饰器

1. 概念介绍

装饰器(decorator),又称“装饰函数”,即一种返回值也是函数的函数,可以称之为“函数的函数”。其目的是在不对现有函数进行修改的情况下,实现额外的功能。最基本的理念来自于一种被称为“装饰模式”的设计模式。

在 Python 中,装饰器属于纯粹的“语法糖”,不使用也没关系,但是使用的话能够大大简化代码,使代码更加易读——当然,是对知道这是怎么回事儿的人而言。

想必经过一段时间的学习,大概率已经在 Python 代码中见过@这个符号。没错,这个符号正是使用装饰器的标识,也是正经的 Python 语法。

语法糖:指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。

2. 运行机制

简单来说,下面两段代码在语义上是可以划等号的(当然具体过程还是有一点微小区别的):

def IAmDecorator(foo):
    '''我是一个装饰函数'''
    pass

@IAmDecorator
def tobeDecorated(...):
    '''我是被装饰函数'''
    pass

与:

def IAmDecorator(foo):
    '''我是一个装饰函数'''
    pass

def tobeDecorated(...):
    '''我是被装饰函数'''
    pass
tobeDecorated = IAmDecorator(tobeDecorated)

可以看到,使用装饰器的@语法,就相当于是将具体定义的函数作为参数传入装饰器函数,而装饰器函数则经过一系列操作,返回一个新的函数,然后再将这个新的函数赋值给原先的函数名。

最终得到的是一个与我们在代码中显式定义的函数同名异质的新函数。

而装饰函数就好像为原来的函数套了一层壳。如图所示,最后得到的组合函数即为应用装饰器产生的新函数:

图片

这里要注意一点,上述两段代码在具体执行上还是存在些微的差异。在第二段代码中,函数名tobeDecorated实际上是先指向了原函数,在经过装饰器修饰之后,才指向了新的函数;但第一段代码的执行就没有这个中间过程,直接得到的就是名为tobeDecorated的新函数。

此外,装饰函数有且只能有一个参数,即要被修饰的原函数。

3. 用法

Python 中,装饰器分为两种,分别是“函数装饰器”和“类装饰器”,其中又以“函数装饰器”最为常见,“类装饰器”则用得很少。

3.1 函数装饰器

3.1.1 大体结构

对装饰函数的定义大致可以总结为如图所示的模板,即:

图片

由于要求装饰函数返回值也为一个函数的缘故,为了在原函数的基础上对功能进行扩充,并且使得扩充的功能能够以函数的形式返回,因此需要在装饰函数的定义中再定义一个内部函数,在这个内部函数中进一步操作。最后return的对象就应该是这个内部函数对象,也只有这样才能够正确地返回一个附加了新功能的函数。

如图一的动图所示,装饰函数就像一个“包装”,将原函数装在了装饰函数的内部,从而通过在原函数的基础上附加功能实现了扩展,装饰函数再将这个新的整体返回。同时对于原函数本身又不会有影响。这也是“装饰”二字的含义。

这个地方如果不定义“内部函数”行不行呢?

答案是“不行”。

3.1.2 关于结构的解释

让我们来看看下面这段代码:

>>> def IAmFakeDecorator(fun):
...     print("我是一个假的装饰器")
...     return fun
...
>>> @IAmFakeDecorator
... def func():
...     print("我是原函数")
...
我是一个假的装饰器

有点奇怪,怎么刚一定义,装饰器扩展的操作就执行了呢?

再来调用一下新函数:

>>> func()
我是原函数

诶呦奇了怪了,扩展功能哪儿去了呀?

不要着急,我们来分析一下上面的代码。在装饰函数的定义中,我们没有另外定义一个内部函数,扩展操作直接放在装饰函数的函数体中,返回值就是传入的原函数。

在定义新函数的时候,下面两段代码又是等价的:

>>> @IAmFakeDecorator
... def func():
...     print("我是原函数")
...
我是一个假的装饰器

>>> def func():
...     print("我是原函数")
...
>>> func = IAmFakeDecorator(func)
我是一个假的装饰器

审视一下后一段代码,我们可以发现,装饰器只在定义新函数的同时调用一次,之后新函数名引用的对象就是装饰器的返回值了,与装饰器没有半毛钱关系。

换句话说,装饰器本身的函数体中的操作都是当且仅当函数定义时,才会执行一次,以后再以新函数名调用函数,执行的只会是内部函数的操作。所以到实际调用新函数的时候,得到的效果跟原函数没有任何区别。

如果不定义内部函数,单纯返回传入的原函数当然也是可以的,也符合装饰器的要求;但却得不到我们预期的结果,对原函数扩展的功能无法复用,只是一次性的。因此这样的行为没有任何意义。

这个在装饰函数内部定义的用于扩展功能的函数可以随意取名,但一般约定俗成命名为wrapper,即“包装”之意。

正确的装饰器定义应如下所示:

>>> def IAmDecorator(fun):
...     def wrapper(*args, **kw):
...         print("我真的是一个装饰器")
...         return fun(*args, **kw)
...     return wrapper
...
3.1.3 参数设置的问题

内部函数参数设置为(*args, **kw)的目的是可以接收任意参数,关于如何接收任意参数的内容在前面的函数参数[1]部分已经介绍过。

之所以要让wrapper能够接收任意参数,是因为我们在定义装饰器的时候并不知道会用来装饰什么函数,具体函数的参数又是什么情况;定义为“可以接收任意参数”能够极大增强代码的适应性。

另外,还要注意给出参数的位置。

要明确一个概念:除了函数头的位置,其他地方一旦给出了函数参数,表达式的含义就不再是“一个函数对象”,而是“一次函数调用”。

因此,我们的装饰器目的是返回一个函数对象,返回语句的对象一定是不带参数的函数名;在内部函数中,我们是需要对原函数进行调用,因此需要带上函数参数,否则,如果内部函数的返回值还是一个函数对象,就还需要再给一组参数才能够调用原函数。Show code:

>>> def IAmDecorator(fun):
...     def wrapper(*args, **kw):
...         print("我真的是一个装饰器")
...         return fun
...     return wrapper
...
>>> @IAmDecorator
... def func(h):
...     print("我是原函数")
...
>>> func()
我真的是一个装饰器
<function func at 0x000001FF32E66950>

原函数没有被成功调用,只是得到了原函数对应的函数对象。只有进一步给出了下一组参数,才能够发生正确的调用(为了演示参数的影响,在函数func的定义中增加了一个参数h):

>>> func()(h=1)
我真的是一个装饰器
我是原函数

只要明白了带参数和不带参数的区别,并且知道你想要的到底是什么效果,就不会在参数上犯错误了。并且也完全不必拘泥上述规则,也许你要的就是一个未经调用的函数对象呢?

把握住这一点,嵌套的装饰器、嵌套的内部函数这些也就都不是问题了。

3.1.4 函数属性

本小节内容启发于廖雪峰的官方网站-Python 教程-函数式编程-装饰器[2]

还应注意的是,经过装饰器的修饰,原函数的属性也发生了改变。

>>> def func():
...     print("我是原函数")
...
>>> func.__name__
'func'

正常来说,定义一个函数,其函数名称与对应的变量应该是一致的,这样在一些需要以变量名标识、索引函数对象时才能够避免不必要的问题。但是事情并不是那么顺利:

>>> @IAmDecorator
... def func():
...     print("我是原函数")
...
>>> func.__name__
'wrapper'

变量名还是那个变量名,原函数还是那个原函数,但是函数名称却变成了装饰器中内部函数的名称。

在这里我们可以使用 Python 内置模块functools中的wraps工具,实现“在使用装饰器扩展函数功能的同时,保留原函数属性”这一目的。这里functools.wraps本身也是一个装饰器。运行效果如下:

>>> import functools
>>> # 定义保留原函数属性的装饰器
... def IAmDecorator(fun):
...     @functools.wraps(fun)
...     def wrapper(*args, **kw):
...         print("我真的是一个装饰器")
...         return fun(*args, **kw)
...     return wrapper
...
>>> @IAmDecorator
... def func():
...     print("我是原函数")
...
>>> func.__name__
'func'

3.2 类装饰器

本节部分参考[Python3 文档-复合语句-类定义[3]]和[python 一篇文章搞懂装饰器所有用法[4]]中类装饰器相关部分

类装饰器的概念与函数装饰器类似,使用上语法也差不多:

@ClassDecorator
class Foo:
    pass

等价于

class Foo:
    pass
Foo = ClassDecorator(Foo)

在定义类装饰器的时候,要保证类中存在__init____call__两种方法。其中__init__方法用以接收原函数或类,__call__方法用以实现装饰逻辑。

简单来讲,__init__方法负责在初始化类实例的时候,将传入的函数或类绑定到这个实例上;而__call__方法则与一般的函数装饰器差不多,连构造都没什么两样,可以认为__call__方法就是一个函数装饰器,因此不再赘述。

3.3 多个装饰器的情况

多个装饰器可以嵌套,具体情况可以理解为从下往上结合的复合函数;或者也可以理解为下一个装饰器的值是前一个装饰器的参数。

举例来说,下面两段代码是等价的:

@f1(arg)
@f2
def func(): 
    pass

def func(): 
    pass
func = f1(arg)(f2(func))

理解了前面的内容,这种情况也很容易掌握。

Python NameSpace & Scope

命名空间定义了在某个作用域内变量名和绑定值之间的对应关系,命名空间是键值对的集合,变量名与值是一一对应关系。作用域定义了命名空间中的变量能够在多大范围内起作用。

命名空间在python解释器中是以字典的形式存在的,是以一种可以看得见摸得着的实体存在的。作用域是python解释器定义的一种规则,该规则确定了运行时变量查找的顺序,是一种形而上的虚的规定。

一、命名空间

1、概述

A namespace is a mapping from names to objects.Most namespaces are currently implemented as Python dictionaries。
命名空间是名字和对象的映射,命名空间是通过 Python Dictionary(字典) 来实现的。

  • 命名空间提供了一个在大型项目下避免名字冲突的方法
  • Python 中各个命名空间都是独立的,他们之间无任何关系
  • 一个命名空间中不能有重名,但不同的命名空间是可以重名而没有任何影响。

命名空间就像是计算机中的文件夹一样,同一个文件夹中的文件不可重名,但是如果两个文件从属于不同的文件夹就可以重名。

图片

同理相同的对象名可以存在不同的命名空间中:

图片

2、命名空间种类

命名空间的种类分为 3 类,命名空间的种类也体现了命名空间的生命周期。三个种类及生命周期描述如下:

1)内置名称(built-in names)

Python 语言内置的名称,比如函数名 abs、char 和异常名称 BaseException、Exception 等等。

生命周期:

对于Python built-in names组成的命名空间,它在Python解释器启动的时候被创建,在解释器退出的时候才被删除;

2)全局名称(global names)

模块中定义的名称,记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。

生命周期:

对于一个Python模块的global namespace,它在这个module被import的时候创建,在解释器退出的时候退出;

3)局部名称(local names)

函数中定义的名称,记录了函数的变量,包括函数的参数和局部定义的变量。(类中定义的也是)

生命周期:

对于一个函数的local namespace,它在函数每次被调用的时候创建,函数返回的时候被删除。

**注意:**命名空间的生命周期取决于对象的作用域,如果对象执行完成,则该命名空间的生命周期就结束。因此,我们无法从外部命名空间访问内部命名空间的对象。例如:

# var1 是全局名称
var1 = 5
def some_func():
 	# var2 是局部名称
    var2 = 6
def some_inner_func():
 	# var3 是内嵌的局部名称
	var3 = 7

命名空间分类图如下:

图片

3、命名空间查找、创建、销毁顺序

3.1 查找变量

如果程序执行时去使用一个变量 hello ,那么 Python, 查找变量顺序为:

局部的命名空间 -> 全局命名空间 -> 内置命名空间

如果按照这个顺序找不到相应的变量,它将放弃查找并抛出一个 NameError 异常:

NameError: name 'hello' is not defined。
3.2 各命名空间创建顺序:

python解释器启动 ->创建内建命名空间 -> 加载模块 -> 创建全局命名空间 ->函数被调用 ->创建局部命名空间

3.3 各命名空间销毁顺序:

函数调用结束 -> 销毁函数对应的局部命名空间 -> python虚拟机(解释器)退出 ->销毁全局命名空间 ->销毁内建命名空间

4、命名空间总结

一个模块的引入,函数的调用,类的定义都会引入命名空间,函数中的再定义函数,类中的成员函数定义会在局部namespace中再次引入局部namespace。

二、作用域

1、概述

A scope is a textual region of a Python program where a namespace is directly accessible. “Directly accessible” here means that an unqualified reference to a name attempts to find the name in the namespace.

作用域就是一个 Python 程序可以直接访问命名空间的正文区域。

  • Python 程序中,直接访问一个变量,会从内到外依次访问所有的作用域直到找到,否则会报未定义的错误。
  • Python 中,程序的变量并不是在哪个位置都可以访问的,访问权限决定于这个变量是在哪里赋值的。
  • Python 中, 变量的作用域决定了在哪一部分程序可以访问哪个特定的变量名称

2、作用域种类

作用域分为4类,分别如下:

  • L(Local):最内层,包含局部变量,比如一个函数/方法内部。
  • E(Enclosing):包含了非局部(non-local)也非全局(non-global)的变量。比如两个嵌套函数,一个函数(或类) A 里面又包含了一个函数 B ,那么对于 B 中的名称来说 A 中的作用域就为 nonlocal。
  • G(Global):当前脚本的最外层,比如当前模块的全局变量。
  • B(Built-in):包含了内建的变量/关键字等,最后被搜索。

作用域规则顺序为:L->E->G->B 如果变量在局部内找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再找不到就去内置中找,如下图所示:

图片

3、全局作用域和局部作用域

局部作用域 (Local)是脚本中的最内层,包含局部变量,比如一个函数或方法内部。闭包函数外函数(Enclosing)包含了非局部(non-local)也非全局(non-global)的变量。全局作用域(Global)是当前脚本的最外层,如当前模块的全局变量,实例如下:

global_scope = 0# 全局作用域
# 定义闭包函数中的局部作用域
def outer():
	o_count = 1# 闭包函数外的函数中,相对于函数 inner() 来说 作用域非局部    
	def inner():
        local_scope = 2# 局部作用域

以上实例展示的是全局作用域和闭包函数中的函数,以及函数中的局部作用域,对于函数 inner() 来说,outer() 中的作用域为 non-local

4、内建作用域

Python 中的内建作用域(Built-in):包含了内建的变量/关键字等,最后被搜索

内建作用域是通过一个名为 builtin 的标准模块来实现的,但是这个变量名自身并没有放入内置作用域内,所以必须导入这个文件才能够使用它。在Python3.0中,可以使用以下的代码来查看到底预定义了哪些变量:

import builtins
dir(builtins)
['ArithmeticError','AssertionError','AttributeError','BaseException','BlockingIOError','BrokenPipeError','BufferError','BytesWarning','ChildProcessError','ConnectionAbortedError','ConnectionError','ConnectionRefusedError'...]

Python 中只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如 if/elif/else/、try/except、for/while等)是不会引入新的作用域的,也就是说这些语句内定义的变量,外部也可以访问,如下:

name1 = 'SuSan'
if chr('SuSan'.__eq__(name1)):
    result = 'I am from China'
else:
    result = 'I am from USA'
print(result)
# 输出结果为:
I am SuSan,I am from China

实例中 result 变量定义在 if 语句块中,但外部还是可以访问的。

如果将 result 定义在函数中,则它就是局部变量,外部不能访问,在代码中会报错运行出异常:

Python Standard Library 01

  • Python 的标准库非常广泛,提供了各种各样的工具。该库包含内置模块(用C编写),可以访问系统功能,例如 Python 程序员无法访问的文件 I / O,以及用 Python 编写的模块,这些模块为许多问题提供标准化解决方案。其中一些模块明确地旨在通过将平台特定的内容抽象为平台中立的 API 来鼓励和增强 Python 程序的可移植性。

  • Python 的标准库(standard library) 是 Python 的一个组成部分,也是 Python 的利器,它可以让编程事半功倍。

1、操作系统接口

1.1 os 模块简介

os 模块提供了很多与操作系统相关联的函数,这使得程序员们在编程的时候能利用函数灵活操作与使用,如果你希望你的程序能够与平台无关的话,运用这个模块中的功能就尤为重要。在使用 os 模块前,需要先 import os 引入模块。以下方法只做介绍,具体的应用可以使用 help(os) 查看帮助文档,最重要的是实际操作。

1.1.1 操作系统相关调用和操作

os.name                        获取操作系统平台
  os.environ                    一个 dictionary 包含环境变量的映射关系
  print(os.environ)             输出环境变量值
  os.system()                   用来运行shell命令
  os.chdir(dir)                 改变当前目录 
  os.chdir(‘F:\WprkSpace’)      注意符号转义
  os.getegid()                  得到有效组id
  os.getgid()                   得到组id
  os.getuid()                   得到用户id
  os.geteuid()                  得到有效用户id
  os.setegid os.setegid() os.seteuid() os.setuid()  设置id
  os.getgruops()                得到用户组名称列表
  os.getlogin()                 得到用户登录名称
  os.getenv                     得到环境变量
  os.putenv                     设置环境变量
  os.umask                      设置umask
  os.system(cmd)                利用系统调用,运行cmd命令

1.1.2 文件目录相关操作

os.getcwd()                   # 获取现在的工作目录
  os.listdir()                  获取某个目录下的所有文件名
  os.remove()                   删除某个文件
  os.path.exists()              检验给出的路径是否真地存在
  os.path.isfile()              判断是否为文件;若是,返回值为真
  os.path.isdir()               判断是否为文件夹;若是,返回值为真
  os.path.abspath(name)         获得绝对路径
  os.path.splitext()            分离文件名与扩展名
  os.path.split()               把一个路径拆分为目录+文件名的形式
  os.path.join(path,name)       连接目录与文件名或目录
  os.path.basename(path)        返回文件名
  os.path.dirname(path)         返回文件路径

1.2 shutil 模块-高级文件操作

shutil 是高级的文件,文件夹,压缩包处理模块。常用方法如下:


# 将文件内容拷贝到另一个文件中
shutil.copyfileobj(fsrc, fdst[, length])

# 拷贝文件
shutil.copyfile(src, dst, *, follow_symlinks=True)

# 仅拷贝权限。内容、组、用户均不变
shutil.copymode(src, dst)

# 仅拷贝状态信息,包括:mode bits, atime, mtime, flags
shutil.copystat(src, dst)

# 拷贝文件和权限
shutil.copy(src, dst)

# 拷贝文件和状态信息
shutil.copy2(src, dst)

# 递归的去拷贝文件夹
shutil.ignore_patterns(*patterns)
shutil.copytree(src, dst, symlinks=False, ignore=None)

# 递归删除文件夹
shutil.rmtree(path[, ignore_errors[, onerror]])

# 递归的去移动文件,它类似mv命令,其实就是重命名。
shutil.move(src, dst)

# 创建压缩包并返回文件路径,例如:zip、tar
shutil.make_archive(base_name, format,...)

2、命令行参数

2.1 sys 模块

通用实用程序脚本通常需要处理命令行参数。这些参数作为列表存储在 sys 模块的 argv 属性中

2.2 argparse 模块

argparse 模块提供了一种处理命令行参数的机制。它应该总是优先于直接手工处理 sys.argv。

3、文件通配符 glob

glob 模块提供了一个在目录中使用通配符搜索创建文件列表的函数

4、错误输出重定向和程序终止

错误输出重定向和终止程序使用 sys 模块,sys 模块还具有 stdin , stdout 和 stderr 的属性。后者对于发出警告和错误消息非常有用,即使在 stdout 被重定向后也可以看到它们。

sys.stderr.write('Warning, log file not found starting a new one\n')
Warning, log file not found starting a new one

终止脚本:

sys.exit()

5、字符串模式匹配

5.1 正则表达式

字符串模式匹配通常也称为正则表达式,使用Python中的 re 标准库,re 模块为高级字符串处理提供正则表达式工具。对于复杂的匹配和操作,正则表达式提供简洁,优化的解决方案,具体的用法后续的文章会单独做详细操作介绍。

6、数学

6.1 math 模块

数学的计算与应用使用 math 模块,math 模块提供了对浮点数学的底层函数访问;

6.2 random 模块

random 模块提供了进行随机选择的工具

6.3 statistics

statistics 模块计算数值数据的基本统计属性(均值,中位数,方差等)

7、互联网访问

有许多模块可用于访问互联网和处理互联网协议。

  • urllib.request 用于从URL检索数据
  • smtplib 用于发送邮件

8、时间和日期

datetime 模块提供了以简单和复杂的方式操作日期和时间的类。虽然支持日期和时间算法,但实现的重点是有效的成员提取以进行输出格式化和操作。该模块还支持可感知时区的对象。

9、数据压缩

Python 中常见的数据存档和压缩格式由模块直接支持,包括:zlib, gzip, bz2, lzma, zipfile 和 tarfile。

10、性能测试

一些Python用户对了解同一问题的不同方法的相对性能产生了浓厚的兴趣。Python提供了一种可以立即回答这些问题的测量工具。

例如,元组封包和拆包功能相比传统的交换参数可能更具吸引力。timeit 模块可以快速演示在运行效率方面一定的优势

>>> from timeit import Timer
>>> Timer('t=a; a=b; b=t', 'a=1; b=2').timeit()
0.57535828626024577
>>> Timer('a,b = b,a', 'a=1; b=2').timeit()
0.54962537085770791

与 timeit 的精细粒度级别相反, profile 和 pstats 模块提供了用于在较大的代码块中识别时间关键部分的工具。

11、质量控制

开发高质量软件的一种方法是在开发过程中为每个函数编写测试,并在开发过程中经常运行这些测试。

11.1 doctest

doctest 模块提供了一个工具,用于扫描模块并验证程序文档字符串中嵌入的测试。测试构造就像将典型调用及其结果剪切并粘贴到文档字符串一样简单。这通过向用户提供示例来改进文档,并且它允许doctest模块确保代码保持对文档的真实

11.2 unittest

unittest 模块不像 doctest 模块那样易于使用,但它允许在一个单独的文件中维护更全面的测试集

12、自带电池

Python 有“自带电池”的理念。Python 自带电池指的是 Python 内置的模块,通过其包的复杂和强大功能可以最好地看到这一点。例如:

  • xmlrpc.client 和 xmlrpc.server 模块使远程过程调用实现了几乎无关紧要的任务。尽管有模块名称,但不需要直接了解或处理XML。
  • email 包是一个用于管理电子邮件的库,包括MIME和其他:基于 RFC 2822 的邮件文档。与 smtplib 和 poplib 实际上发送和接收消息不同,电子邮件包具有完整的工具集,用于构建或解码复杂的消息结构(包括附件)以及实现互联网编码和标头协议。
  • json 包为解析这种流行的数据交换格式提供了强大的支持。
  • csv 模块支持以逗号分隔值格式直接读取和写入文件,这些格式通常由数据库和电子表格支持。
  • sqlite3 模块是SQLite数据库库的包装器,提供了一个可以使用稍微非标准的SQL语法更新和访问的持久数据库。

Python Standard Library 02

  • Python 的标准库非常广泛,提供了各种各样的工具。该库包含内置模块(用C编写),可以访问系统功能。
  • Python 的标准库(standard library) 是 Python 的一个组成部分,也是 Python 的利器,它可以让编程事半功倍。
  • Python 标准库第二部分涵盖的模块是包含在 Python 高级编程中,这一部分所涉及的模块很少运用在脚本中

13、格式化输出

13.1 reprlib 模块

reprlib 模块提供了一个定制化版本的 repr() 函数,用于缩略显示大型或深层嵌套的容器对象,将容器中的对象按照一定的规律输出 reprlib 模块包含了一个类、一实例对象、一方法

  1. class reprlib.Repr

    Repr类, 该类提供格式化服务,对于实现与内置的 repr() 类似的函数很有用;添加了不同对象类型的大小限制,以避免生成过长的表示。

  2. reprlib.aRepr

    Repr 类的实例,用于提供下面描述的 Repr() 函数。更改此对象的属性将影响 repr() 和 Python 调试器使用的大小限制。

  3. reprlib.repr(obj)

    这是 aRepr 的 repr() 方法。它返回一个与内置同名函数返回的字符串类似的字符串,但对大多数大小都有限制

  4. @reprlib.recursive_repr(fillvalue=“…”)

    方法的装饰器,用于检测同一线程中的递归调用。如果执行递归调用,则返回fillvalue,否则执行通常的调用。

例如:

# 导入模块
from reprlib import recursive_repr
class MyList(list):
    @recursive_repr()
    def __repr__(self):
        return '<' + '|'.join(map(repr, self)) + '>'
m = MyList('abc')
m.append(m)
m.append('x')
print(m)

输出结果为:

<'a'|'b'|'c'|...|'x'>

Repr 对象具有的属性

  • Repr.maxlevel — 递归表示的深度限制,默认是6
  • Repr.maxdict
  • Repr.maxlist
  • Repr.maxtuple
  • Repr.maxset
  • Repr.maxfrozenset
  • Repr.maxdeque
  • Repr.maxarray ----命名对象类型的条目数限制,maxdict是4,maxarray是5,其它是6
  • Repr.maxlong ---- 表示一个整数最大字符数,默认40
  • Repr.maxstring ---- 表示一个字符串最大字符数,默认30
  • Repr.maxother ---- 表示其他类型的最大字符数,默认20

例如:

# 递归实例演示
import reprlib
a = [1,2,3,[4,5],6,7]
reprlib.aRepr.maxlevel = 1
print(reprlib.repr(a))

输出结果为:

[1, 2, 3, [...], 6, 7]

13.2 pprint 模块

pprint 模块提供了更加复杂的打印控制,其输出的内置对象和用户自定义对象能够被解释器直接读取。当输出结果过长而需要折行时,“美化输出机制”会添加换行符和缩进,以更清楚地展示数据结构

13.3 textwrap 模块

textwrap 模块能够格式化文本段落,以适应给定的屏幕宽度, 该模块提供了一些便利功能以及可以完成所有工作的类。如果只是包装或填充一个或两个文本字符串,那么便利功能应该足够好;否则应该使用一个模块化功能提高效率。

13.4 locale 模块

locale 模块处理与特定地域文化相关的数据格式。locale 模块的 format 函数包含一个 grouping 属性,可直接将数字格式化为带有组分隔符的样式

14、string 模板

string 模块包含一个通用的 Template 类,具有适用于最终用户的简化语法。它允许用户在不更改应用逻辑的情况下定制自己的应用

15、使用二进制数据记录格式

struct 模块提供了 pack() 和 unpack() 函数,用于处理不定长度的二进制记录格式。下面的例子展示了在不使用 zipfile 模块的情况下,如何循环遍历一个 ZIP 文件的所有头信息。Pack 代码 “H” 和 “I” 分别代表两字节和四字节无符号整数。“<” 代表它们是标准尺寸的小尾型字节序

16、多线程

线程是一种对于非顺序依赖的多个任务进行解耦的技术。多线程可以提高应用的响应效率,当接收用户输入的同时,保持其他任务在后台运行。一个有关的应用场景是,将 I/O 和计算运行在两个并行的线程中,在程序编写过程中,熟悉多线程的应用能提高代码运行效率

17、日志

Python 日志模块 logging 模块提供功能齐全且灵活的日志记录系统。在最简单的情况下,日志消息被发送到文件或 sys.stderr。日志系统可以直接从 Python 配置,也可以从用户配置文件加载,以便自定义日志记录而无需更改应用程序。

18、弱引用

Python 会自动进行内存管理(对大多数对象进行引用计数并使用 garbage collection 来清除循环引用)。当某个对象的最后一个引用被移除后不久就会释放其所占用的内存

19、用于操作列表的工具

许多对于数据结构的需求可以通过内置列表 类型来满足。但是,有时也会需要具有不同效费比的替代实现。

19.1 array 模块

array 模块提供了一种 array() 对象,它类似于列表,但只能存储类型一致的数据且存储密集更高。下面的例子演示了一个以两个字节为存储单元的无符号二进制数值的数组 (类型码为 “H”),而对于普通列表来说,每个条目存储为标准 Python 的 int 对象通常要占用16 个字节:

>>> from array import array
>>> a = array('H', [4000, 10, 700, 22222])
>>> sum(a)
26932
>>> a[1:3]
array('H', [10, 700])

19.2 collections 模块

collections 模块提供了一种 deque() 对象,它类似于列表,但从左端添加和弹出的速度较快,而在中间查找的速度较慢。此种对象适用于实现队列和广度优先树搜索:


>>>
>>> from collections import deque
>>> d = deque(["task1", "task2", "task3"])
>>> d.append("task4")
>>> print("Handling", d.popleft())
Handling task1
unsearched = deque([starting_node])
def breadth_first_search(unsearched):
    node = unsearched.popleft()
    for m in gen_moves(node):
        if is_goal(m):
            return m
        unsearched.append(m)

在替代的列表实现以外,标准库也提供了其他工具,例如 bisect 模块具有用于操作排序列表的函数:

>>> import bisect
>>> scores = [(100, 'perl'), (200, 'tcl'), (400, 'lua'), (500, 'python')]
>>> bisect.insort(scores, (300, 'ruby'))
>>> scores
[(100, 'perl'), (200, 'tcl'), (300, 'ruby'), (400, 'lua'), (500, 'python')]

19.3 heapq 模块

heapq 模块提供了基于常规列表来实现堆的函数。最小值的条目总是保持在位置零。这对于需要重复访问最小元素而不希望运行完整列表排序的应用来说非常有用:

>>> from heapq import heapify, heappop, heappush
>>> data = [1, 3, 5, 7, 9, 2, 4, 6, 8, 0]
>>> heapify(data)                      # rearrange the list into heap order
>>> heappush(data, -5)                 # add a new entry
>>> [heappop(data) for i in range(3)]  # fetch the three smallest entries
[-5, 0, 1]

20、十进制浮点运

decimal 模块提供了一种 Decimal 数据类型用于十进制浮点运算。相比内置的 float 二进制浮点实现,该类特别适用于以下几种场景:

  • 财务应用和其他需要精确十进制表示的用途,
  • 控制精度,
  • 控制四舍五入以满足法律或监管要求,
  • 跟踪有效小数位,或
  • 用户期望结果与手工完成的计算相匹配的应用程序。

例如,使用十进制浮点和二进制浮点数计算 70 美分手机和 5% 税的总费用,会产生的不同结果。如果结果四舍五入到最接近的分数差异会更大

Python datetime 和 time

在前面的章节中已经介绍了一些 Python 常用的模块,本节再介绍两个模块 datetime 模块和 time 模块,这两个模块主要用于转换日期格式的功能。

datetime模块

datetime 模块是 Python 内置的功能模块,它可以实现对日期的算数运算,以指定的方式格式化日期。datetime 模块内含有一个同名的 datetime 类,该类中包含多个操作日期的函数,例如:datetime.now()、datetime.fromtimestamp()、datetime.timedelta()等,下面逐一举例说明。

datetime()构造函数

datetime 类提供了一个now()的方法可以获取当前日期和时间,还提供了带参数的构造函数datetime(),可以通过传入特定的数字返回不同的datetime 对象。例如:

import datetime
#当前日期和时间
print(datetime.datetime.now())
2019-09-30 22:19:37.582514

#获取指定时间
datetest = datetime.datetime(2019,9,30,22,22,0)
print(datetest)
2019-09-30 22:22:00

#获取日期的年月日时分秒
print(str(datetest.year)+"-"+str(datetest.month)+"-"+str(datetest.day)+" "+str(datetest.hour)+":"+str(datetest.minute)+":"+str(datetest.second))
2019-9-30 22:22:0

fromtimestamp()函数

fromtimestamp()函数可以将时间戳转换成 datetime 对象。例如:

import datetime
dt1 = datetime.datetime.fromtimestamp(10000)
dt2 = datetime.datetime.fromtimestamp(time.time())

print(dt1)
print(dt2)

1970-01-01 10:46:40
2019-09-30 23:28:47.629210

strptime()和strftime()函数

使用strptime()函数可以将日期字符串转换成 datetime 类型,strftime()函数可以将 datetime 类型转换成字符串。例如:

import datetime
#日期转换
datestr = datetime.strptime('2019-9-30 22:10:00', '%Y-%m-%d %H:%M:%S')
now = datetime.now()
print(datestr)
print(now.strftime('%a, %b %d %H:%M'))

2019-09-30 22:10:00
Tue, Oct 01 00:02

timedelta()函数

timedelta()函数返回一个 timedelta 类型的数据,它表示一段时间而不是一个时刻,多用于日期的增加和减少场景。例如:

import datetime
#日期增加和减少
now = datetime.datetime.now()
print(now)

newdate = now + datetime.timedelta(hours=10)
print(newdate)

newdate = now - datetime.timedelta(days=1)
print(newdate)

2019-10-01 00:23:50.152118
2019-10-01 10:23:50.152118
2019-09-30 00:23:50.152118

time模块

与 datetime 模块有所不同,time 模块主要功能是读取系统时钟的当前时间。其中,time.time() 和 time.sleep() 是两个最常用的模块。

time()函数

time.time() 函数返回的值是带小数点的,它表示从 Unix 纪元(1970年1月1日0点)到执行代码那一刻所经历的时间的秒数,这个数字称为UNIX纪元时间戳。例如:

import time
print ("当前时间戳为:", time.time())

当前时间戳为: 1569770357.6496012

在项目开发中,我们经常需要计算一段代码的执行时间,就可以用纪元时间戳来实现。例如:

import time
def calculateTime():
    item = 1
    for i in range(1,100000):
        item = item + i
    return item

startTime = time.time()
result = calculateTime()
endTime = time.time()
print('计算结果:'+ str(result))
print('执行时间:'+ str(endTime - startTime))

计算结果:4999950001
执行时间:0.020943403244018555

在代码中,函数calculateTime()是需要执行的代码块,变量 startTime 表示开始时间,变量 endTime 表示结束时间,endTime-startTime表示代码块运行的间隔时间。

sleep()函数

如果需要让程序暂停一下,可以使用time.sleep()函数。sleep()函数有个参数,表示需要暂停的秒数。例如:

import time
for i in range(2):
    print('one')
    print(time.time())
    time.sleep(1)
    print('two')
    print(time.time())
    time.sleep(1)
print('运行完成')

one
1569772121.6350794
two
1569772122.637142
one
1569772123.639813
two
1569772124.6423109
运行完成

从上面程序的执行结果可以看出以下几点:

  1. 打印one和打印two之间每次都间隔了一秒,因为time.time()函数输出结果的精确度比较高,会存在些许误差。
  2. time.sleep()函数会阻塞代码,只有当time.sleep()中的秒数流逝后,才会执行后续代码

Python 垃圾回收机制

众所周知,Python 是一门面向对象语言,在 Python 的世界一切皆对象。所以一切变量的本质都是对象的一个指针而已。

Python 运行过程中会不停的创建各种变量,而这些变量是需要存储在内存中的,随着程序的不断运行,变量数量越来越多,所占用的空间势必越来越大,如果对变量所占用的内存空间管理不当的话,那么肯定会出现 out of memory。程序大概率会被异常终止。

因此,对于内存空间的有效合理管理变得尤为重要,那么 Python 是怎么解决这个问题的呢。其实很简单,对不不可能再使用到的内存进行回收即可,像 C 语言中需要程序员手动释放内存就是这个道理。但问题是如何确定哪些内存不再会被使用到呢?这就是我们今天要说的垃圾回收了。

目前垃圾回收比较通用的解决办法有三种,引用计数,标记清除以及分代回收。

引用计数

引用计数也是一种最直观,最简单的垃圾收集技术。在 Python 中,大多数对象的生命周期都是通过对象的引用计数来管理的。其原理非常简单,我们为每个对象维护一个 ref 的字段用来记录对象被引用的次数,每当对象被创建或者被引用时将该对象的引用次数加一,当对象的引用被销毁时该对象的引用次数减一,当对象的引用次数减到零时说明程序中已经没有任何对象持有该对象的引用,换言之就是在以后的程序运行中不会再次使用到该对象了,那么其所占用的空间也就可以被释放了了。

我们来看看下面的例子。

import os
import psutil


# 打印当前程序占用的内存大小
def print_memory_info(name):
    pid = os.getpid()
    p = psutil.Process(pid)

    info = p.memory_full_info()
    MB = 1024 * 1024
    memory = info.uss / MB
    print('%s used %d MB' % (name, memory))

# 测试函数
def foo():
    print_memory_info("foo start")
    length = 1000 * 1000
    list = [i for i in range(length)]
    print_memory_info("foo end")


foo()
print_memory_info("main end")

### 输出结果
foo start used 6 MB
foo end used 55 MB
main end used 10 MB

函数 print_memory_info 用来获取程序占用的内存空间大小,在 foo 函数中创建一个包含一百万个整数的列表。从打印结果我们可以看出,创建完列表之后程序耗用的内存空间上升到了 55 MB。而当函数 foo 调用完毕之后内存消耗又恢复正常。

这是因为我们在函数 foo 中创建的 list 变量是局部变量,其作用域是当前函数内部,一旦函数执行完毕,局部变量的引用会被自动销毁,即其引用次数会变为零,所占用的内存空间也会被回收。

为了验证我们的想法,我们对函数 foo 稍加改造。代码如下:

def foo():
    print_memory_info("foo start")
    length = 1000 * 1000
    list = [i for i in range(length)]
    print_memory_info("foo end")
    return list

### 输出结果
foo start used 6 MB
foo end used 55 MB
main end used 55 MB

稍加改造之后,即使 foo 函数调用结束其所消耗的内存也未被释放。

主要是因为我们将函数 foo 内部产生的列表返回并在主程序中接收之后,这样就会导致该列表的引用依然存在,该对象后续仍有可能被使用到,垃圾回收便不会回收该对象。

那么,什么时候对象的引用次数才会增加呢。下面四种情况都会导致对象引用次数加一。

  • 对象被创建(num=2)
  • 对象被引用(count=num)
  • 对象作为参数传递到函数内部
  • 对象作为一个元素添加到容器中

同理,对象引用次数减一的情况也有四种。

  • 对象的别名被显式销毁(del num)
  • 对象的别名被赋予新的对象(num=30)
  • 对象离开它的作用域(函数局部变量)
  • 从容器中删除对象,或者容器被销毁

引用计数看起来非常简单,实现起来也不复杂,只需要维护一个字段保存对象被引用的次数即可,那么是不是就代表这种算法没有缺点了呢。实则不然,我们知道引用次数为零的对象所占用的内存空间肯定是需要被回收的。那引用次数不为零的对象呢,是不是就一定不能回收呢?

我们来看看下面的例子,只是对函数 foo 进行了改造,其余未做更改。

def foo():
    print_memory_info("foo start")
    length = 1000 * 1000
    list_a = [i for i in range(length)]
    list_b = [i for i in range(length)]
    list_a.append(list_b)
    list_b.append(list_a)
    print_memory_info("foo end")
    return list

### 输出结果
foo start used 6 MB
foo end used 93 MB
main end used 93 MB

我们看到,在函数 foo 内部生成了两个列表 list_a 和 list_b,然后将两个列表分别添加到另外一个中。由结果可以看出,即使 foo 函数结束之后其所占用的内存空间依然未被释放。这是因为对于 list_a 和 list_b 来说虽然没有被任何外部对象引用,但因为二者之间交叉引用,以至于每个对象的引用计数都不为零,这也就造成了其所占用的空间永远不会被回收的尴尬局面。这个缺点是致命的。

为了解决交叉引用的问题,Python 引入了标记清除算法和分代回收算法。

标记清除

显然,可以包含其他对象引用的容器对象都有可能产生交叉引用问题,而标记清除算法就是为了解决交叉引用的问题的。

标记清除算法是一种基于对象可达性分析的回收算法,该算法分为两个步骤,分别是标记和清除。标记阶段,将所有活动对象进行标记,清除阶段将所有未进行标记的对象进行回收即可。那么现在的问题变为了 GC 是如何判定哪些是活动对象的?

事实上 GC 会从根结点出发,与根结点直接相连或者间接相连的对象我们将其标记为活动对象(该对象可达),之后进行回收阶段,将未标记的对象(不可达对象)进行清除。前面所说的根结点可以是全局变量,也可以是调用栈。

标记清除算法主要用来处理一些容器对象,虽说该方法完全可以做到不误杀不遗漏,但 GC 时必须扫描整个堆内存,即使只有少量的非可达对象需要回收也需要扫描全部对象。这是一种巨大的性能浪费。

分代回收

由于标记清除算法需要扫描整个堆的所有对象导致其性能有所损耗,而且当可以回收的对象越少时性能损耗越高。因此 Python 引入了分代回收算法,将系统中存活时间不同的对象划分到不同的内存区域,共三代,分别是 0 代,1 代 和 2 代。新生成的对象是 0 代,经过一次垃圾回收之后,还存活的对象将会升级到 1 代,以此类推,2 代中的对象是存活最久的对象。

那么什么时候触发进行垃圾回收算法呢。事实上随着程序的运行会不断的创建新的对象,同时也会因为引用计数为零而销毁大部分对象,Python 会保持对这些对象的跟踪,由于交叉引用的存在,以及程序中使用了长时间存活的对象,这就造成了新生成的对象的数量会大于被回收的对象数量,一旦二者之间的差值达到某个阈值就会启动垃圾回收机制,使用标记清除算法将死亡对象进行清除,同时将存活对象移动到 1 代。以此类推,当二者的差值再次达到阈值时又触发垃圾回收机制,将存活对象移动到 2 代。

这样通过对不同代的阈值做不同的设置,就可以做到在不同代使用不同的时间间隔进行垃圾回收,以追求性能最大。

事实上,所有的程序都有一个相似的现象,那就是大部分的对象生存周期都是相当短的,只有少量对象生命周期比较长,甚至会常驻内存,从程序开始运行持续到程序结束。而通过分代回收算法,做到了针对不同的区域采取不同的回收频率,节约了大量的计算从而提高 Python 的性能。

除了上面所说的差值达到一定阈值会触发垃圾回收之外,我们还可以显示的调用 gc.collect() 来触发垃圾回收,最后当程序退出时也会进行垃圾回收。

Python 到底是值传递还是引用传递

我们平时写的 Python 程序中充斥着大量的函数,包括系统自带函数和自定义函数,当我们调用函数时直接将参数传递进去然后坐等接收返回值即可,简直不要太好用。那么你知道函数的参数是怎么传递的么,是值传递还是引用传递呢,什么又是值传递和引用传递呢?

这个问题对于很多初学者还是比较有难度的,看到这里你可以稍加停顿,自己思考一下,看看自己是否真正理解了。很多人只是知道概念但是让他说他又说不清楚,思考过后如果你还觉得模糊的话,往下仔细看,我今天就带着你深入剖析下函数的参数传递机制。

为了搞清楚函数的参数传递机制,你必须先彻底理解形参和实参。例如下面的 sayHello 函数,括号里面的 name 就是形参,而当调用函数时传递的 name 是实参。

def sayHello(name): # name 是形式参数
    print("Hello %s" % name)


name = "hanmeimei" # name 是实际参数
sayHello(name)

# 输出结果
Hello hanmeimei

值传递 OR 引用传递

上面我们说了,当调用函数时我们会把实际参数传递给形式参数。而这个传递过程有两种,就是我们上文说的值传递和引用传递了。

顾名思义,所谓值传递就是指在传递过程中将实际参数的值复制一份传递给形式参数,这样即使在函数执行过程中对形式参数进行了修改,形式参数也不会有所改变,因为二者互不干扰。而引用传递是值将实际参数的引用传递给实际参数,这样二者就会指向同一块内存地址,在函数执行过程中对形式参数进行了修改,形式参数也会一并=被修改。

为了故事的顺利发展,我们先来看看 Python 中关于变量的赋值。

>>> a = 10
>>> b = a
>>> a = a + 10
>>> a
20
>>> b
10
>>>

在上述的例子中,我们声明了一个变量 a,其值为 10,然后将 b 也指向 a,这是在内存中的布局是这样的,变量 a 和 b 会指向同一个对象 10,而不是给 b 重新生成一个新的对象。

图片

由此可知,同一个对象是可以被多个对象引用的。

当执行完 a = a + 10 后,因为整数是不可变对象,所以并不会将 10 变成 20,而是生成一个新的对象 20 ,然后 a 会指向这个新的对象。b 还是指向旧对象 10。

图片

所以,最后就是 a 为 20,而 b 为 10。

理解了上面的赋值过程之后,我们再来看看参数的传递。老规矩,还是直接看例子吧,代码是不会骗人的。

def swap(a, b):
    a, b = b, a
    print("in swap a = %d and b = %d " % (a, b))


a = 100
b = 200
swap(a, b)
print("in main a = %d and b = %d " % (a, b))

## 输出结果
in swap a = 200 and b = 100 
in main a = 100 and b = 200

我们在函数 swap 中交换 a 和 b 的值,然后分别在主函数和 swap 函数中输出其结果,由结果可知,swap 函数并不会改变实际参数 a,b 的值,因此我们可以得出结论,Python 函数参数是按照值传递的。

别急,不妨再看多一个例子。

def swap(list):
    list.append(4)
    print("in swap list is %s " % list)


list_x = [1, 2, 3]
swap(list_x)
print("in main list is %s " % list_x)

## 输出结果
in swap list is [1, 2, 3, 4] 
in main list is [1, 2, 3, 4]

咦,值被改了,这不就是引用传递了么。于是,我们又得出结论,Python 函数参数是按照引用传递的。

这未免有点太不严谨了,事实上我们上面的第二个例子有点不太严谨,我稍微修改了下 swap 函数,咱们在看看测试结果。

def swap(list):
    list = list + [4]
    print("in swap list is %s " % list)

## 输出结果
in swap list is [1, 2, 3, 4] 
in main list is [1, 2, 3]

我们只是更改了 swap 函数内一行代码,结果就完全不一样了。为了更好的理解其执行过程,我画了张图。

图片

在第一个关于 list 的例子中,我们首先声明了一个列表,其中的元素为 [1,2,3],此时其内存布局如上图中的步骤一所示。list_x 指向内存地址为 OX7686934F 的区域。

当调用 swap 函数将 list_x 传递给形式参数 list 时,会将该地址直接传递过去,list 也会指向这个地址,如步骤二所示。

最后,由于列表是可变的,所以当 list 在向列表中添加元素时,list_x 自然会受到影响,因为二者指向的是同一块内存。

所以,这里也是值传递,只不过传递的值是对象的内存地址罢了。

第二个关于 list 的例子中,我们对 swap 函数进行了修改,其执行流程如下图所示。

图片

在执行 swap 函数之前都与上面的例子毫无差别。在 swap 函数内部 list = list + [4] 表示新建一个末尾加入元素 4 的新的列表,并让 list 指向这个新的内存地址 OX7686936A。因为是生成了一个新的对象,与原对象无关,所以 list_x 不受影响。

简而言之,弄清楚改变变量和重新赋值的区别就好了,第一个例子中我们改变了变量的值,所以当函数执行结束后所有指向该对象的变量都会受影响。而重新赋值相当于重新生成一个新的对象并在新的对象上做操作,因此旧对象不受影响。

如果我们要想在函数中改变对象,第一可以传入可变数据类型(列表,字典,集合),直接改变;第二还可以创建一个新的对象,修改后返回。建议用后者,表达清晰明了,不易出错。

Python 之对象的比较与拷贝

众所周知,Python 是一门面向对象语言,在 Python 的世界一切皆对象,那么我们如何判断两个对象是否是同一个对象呢。

== 操作符和 is

相信大家对于这两个操作符都不陌生。具体来说就是 == 操作符比较的是两个对象的值是否相等,而 is 操作符的含义则是二者到底是否是同一个对象,换言之,即两个对象是否指向同一块内存地址。

上面我们说过,Python 中一切皆是对象,对象包含 id(唯一身份标识),type(类型) 和 value(值) 三个要素。id 可以通过函数 id(obj) 来获取。因此 is 操作符就相当于比较两个对象的 id 是否相同,而 == 操作符则相当于比较两个对象的 value 是否相同。

下面我们来看几个例子。

>>> a = 'red'
>>> b = 'red'
>>> a == b
True

这里,我们声明了两个对象 a 和 b,其内容都是字符串 ‘red’,毋庸置疑 == 操作符应该返回 True,很好理解。

>>> a = 256
>>> b = 256
>>> a == b
True
>>> a is b
True

>>> a = 257
>>> b = 257
>>> a == b
True
>>> a is b
False

同样,对于 == 操作符,无论是 a,b 的值是 256 还是 257 二者都是相等的。奇怪的是同样是 is 操作,数值的大小居然对结果有影响。

事实上 a is b 为 True 的结论只适用于 -5 到 256 的数值,因为出于性能的考虑,Python 对这个范围内的数值进行了缓存。当你为整数对象赋值时(-5 到 256)并不会生成新的对象,而是使用事先创建好的缓存对象。如果超过了缓存范围,那么就会申请两块不同的内存地址,is 操作当然会返回 False。

不信我们可以把它们的 id 拿出来看看。

>>> a = 256
>>> b = 256
>>> id(a)
4525792016
>>> id(b)
4525792016

>>> a = 257
>>> b = 257
>>> id(a)
4528947760
>>> id(b)
4528947856

但是,当你把同样的代码放到编辑器中去去执行时,你会惊奇的发现程序的执行结果跟我们刚才所说的缓存机制竟然是相冲突的。

图片

不是说 -5 到 256 范围内的整数才会被缓存么。为啥这么大的数 is 操作也返回 True 了呢。

从结果来看 a 和 b 两个大数的内存地址肯定是一样的,不然 is 操作符也不会返回 True。这是因为在交互模式(就是那个黑窗口了)下每一条命令就是一个代码块,Python 逐行编译执行;在编辑器中,一个函数,一个类或者一个文件才是一个代码块。Python 会整体编译执行,因此相同值的变量只会初始化一次,第二次初始化相同值的变量时会重用旧值。

上面所说的编译是指 CPython 将源代码编译为字节码的过程。

只有当对象的值是数值或者字符串型且在缓存范围内时,a is b 才返回 True,否则当 a 和 b 是 int,str,list,tuple,set 或 dict 类型时,a is b 均返回 False。

事实上,经过测试,我发现对于有空格的字符串 Python 并不会去缓存。找了好久,终于在 stringobject.h 中找到了解释,Python 解释器采取 intern 机制来提高字符串操作效率,当内存中有相同值的字符串时就会重用,而不是生成一个新的相同值的字符串对象。但也不是说对所有的字符串均采取该 intern 机制。只有看起来像 Python 标识符的字符串才会被缓存。

This is generally restricted to strings that “look like” Python identifiers, although the intern() builtin can be used to force interning of any string.

另外,比较操作符 is 的效率要优于 ==,因为 is 操作符无法被重载,执行 is 操作只是对比对象的 id 而已。而 == 操作符则会递归地遍历对象的所有值,并逐一比较。

题外话:如果你了解 Java 就会发现,Python 中的 == 类似于 Java 中的 equals,而 is 则类似与 Java 中的 == 比较符。

对象的拷贝

对象的拷贝其实就是创建新的对象的过程,在 Python 中共有两种拷贝模式,浅拷贝和深拷贝。

当顶层对象和它的子元素对象全都是不可变对象时,不存在被拷贝,因为没有产生新对象。浅拷贝和深拷贝的区别就是浅拷贝只拷贝顶层对象,而不会去拷贝内部的子元素对象。深拷贝则会递归地拷贝顶层对象内部的子元素对象。

我们可以使用对象类型本身的构造器,切片 以及 copy 函数来实现浅拷贝。

a = [1, 'hello', [1,2]]
b = list(a)

a[0] = 100
a[2][0] = 100
print(a)
print(b)

## 输出结果
[100, 'hello', [100, 2]]
[1, 'hello', [100, 2]]

对于顶层可变的对象,如果其子对象不可变,那当你修改子对象时,实际上是将引用指向了另外一个新的对象而已。类比上边的例子就是并不是把 a[0] 的值从 1 修改为 100,而是将 a[0] 指向 100。

如果子对象可变,比如对于 a[2] 来说,由于是浅拷贝,所以实际上 a[2]b[2] 指向的都是同一个列表对象。修改 a[2][0] 为 100之后,b[2][0] 也一并会修改。

通过切片来实现浅拷贝。

>>> a = [1, 2, 3]
>>> b = a[:]
>>> a == b
True
>>> a is b
False

但是对于顶层不可变的对象,不存在对象的拷贝,因为都是指向同一个对象,没有新的对象产生。

>>> a = (1,2,3)
>>> b = tuple(a)
>>> a == b
True
>>> a is b
True

如你所见,关于浅拷贝如果元素不可变的还好,没什么副作用;如果元素可变,那就要小心其副作用了。

深拷贝递归拷贝顶层对象以及它内部的子对象,因此,新对象和原来的旧对象,没有任何关联。Python 中使用 copy.deepcopy() 函数来实现对对象的深拷贝。

import copy

a = [1, 'hello', [1,2]]
b = copy.deepcopy(a)

a[0] = 100
a[2][0] = 100
print(a)
print(b)

## 输出结果
[100, 'hello', [100, 2]]
[1, 'hello', [1, 2]]

深拷贝即拷贝了顶层对象,同时还拷贝了子对象,所以 a[2]b[2] 指向的是两个不同的列表。修改 a[2][0] 后,重新指向了新的整数,但是这并不会影响到 b[2]

>>> import copy
>>> a = (1,2,3)
>>> b = copy.deepcopy(a)
>>> a is b
True

>>> a = (1,2,[1,2])
>>> b = copy.deepcopy(a)
>>> a is b
False
>>> a[2][0] = 100
>>> a
(1, 2, [100, 2])
>>> b
(1, 2, [1, 2])

对于不可变对象来说,如果其子对象全部不可变,那么深拷贝就和浅拷贝是一样的效果,都是指向同一个内存地址。

但是如果子对象中包含可变对象,那么深拷贝之后的对象就不再是原来的对象了,因为可变对象被重新拷贝了一份,放到例子中就是 a[2]b[2] 指向的不再是同一个列表。因此,修改 a[2] 并不会影响 b[2]

如果深拷贝的对象中存在指向自身的引用,那么会不会无限递循环呢。

a = 257
b = 257
a == b
True
a is b
False


同样,对于 `==` 操作符,无论是 a,b 的值是 256 还是 257 二者都是相等的。奇怪的是同样是 is 操作,数值的大小居然对结果有影响。

事实上 a is b 为 True 的结论只适用于 -5 到 256 的数值,因为出于性能的考虑,Python 对这个范围内的数值进行了缓存。当你为整数对象赋值时(-5 到 256)并不会生成新的对象,而是使用事先创建好的缓存对象。如果超过了缓存范围,那么就会申请两块不同的内存地址,is 操作当然会返回 False。

不信我们可以把它们的 id 拿出来看看。

```python
>>> a = 256
>>> b = 256
>>> id(a)
4525792016
>>> id(b)
4525792016

>>> a = 257
>>> b = 257
>>> id(a)
4528947760
>>> id(b)
4528947856

但是,当你把同样的代码放到编辑器中去去执行时,你会惊奇的发现程序的执行结果跟我们刚才所说的缓存机制竟然是相冲突的。

[外链图片转存中…(img-PBgn5b8b-1700363329726)]

不是说 -5 到 256 范围内的整数才会被缓存么。为啥这么大的数 is 操作也返回 True 了呢。

从结果来看 a 和 b 两个大数的内存地址肯定是一样的,不然 is 操作符也不会返回 True。这是因为在交互模式(就是那个黑窗口了)下每一条命令就是一个代码块,Python 逐行编译执行;在编辑器中,一个函数,一个类或者一个文件才是一个代码块。Python 会整体编译执行,因此相同值的变量只会初始化一次,第二次初始化相同值的变量时会重用旧值。

上面所说的编译是指 CPython 将源代码编译为字节码的过程。

只有当对象的值是数值或者字符串型且在缓存范围内时,a is b 才返回 True,否则当 a 和 b 是 int,str,list,tuple,set 或 dict 类型时,a is b 均返回 False。

事实上,经过测试,我发现对于有空格的字符串 Python 并不会去缓存。找了好久,终于在 stringobject.h 中找到了解释,Python 解释器采取 intern 机制来提高字符串操作效率,当内存中有相同值的字符串时就会重用,而不是生成一个新的相同值的字符串对象。但也不是说对所有的字符串均采取该 intern 机制。只有看起来像 Python 标识符的字符串才会被缓存。

This is generally restricted to strings that “look like” Python identifiers, although the intern() builtin can be used to force interning of any string.

另外,比较操作符 is 的效率要优于 ==,因为 is 操作符无法被重载,执行 is 操作只是对比对象的 id 而已。而 == 操作符则会递归地遍历对象的所有值,并逐一比较。

题外话:如果你了解 Java 就会发现,Python 中的 == 类似于 Java 中的 equals,而 is 则类似与 Java 中的 == 比较符。

对象的拷贝

对象的拷贝其实就是创建新的对象的过程,在 Python 中共有两种拷贝模式,浅拷贝和深拷贝。

当顶层对象和它的子元素对象全都是不可变对象时,不存在被拷贝,因为没有产生新对象。浅拷贝和深拷贝的区别就是浅拷贝只拷贝顶层对象,而不会去拷贝内部的子元素对象。深拷贝则会递归地拷贝顶层对象内部的子元素对象。

我们可以使用对象类型本身的构造器,切片 以及 copy 函数来实现浅拷贝。

a = [1, 'hello', [1,2]]
b = list(a)

a[0] = 100
a[2][0] = 100
print(a)
print(b)

## 输出结果
[100, 'hello', [100, 2]]
[1, 'hello', [100, 2]]

对于顶层可变的对象,如果其子对象不可变,那当你修改子对象时,实际上是将引用指向了另外一个新的对象而已。类比上边的例子就是并不是把 a[0] 的值从 1 修改为 100,而是将 a[0] 指向 100。

如果子对象可变,比如对于 a[2] 来说,由于是浅拷贝,所以实际上 a[2]b[2] 指向的都是同一个列表对象。修改 a[2][0] 为 100之后,b[2][0] 也一并会修改。

通过切片来实现浅拷贝。

>>> a = [1, 2, 3]
>>> b = a[:]
>>> a == b
True
>>> a is b
False

但是对于顶层不可变的对象,不存在对象的拷贝,因为都是指向同一个对象,没有新的对象产生。

>>> a = (1,2,3)
>>> b = tuple(a)
>>> a == b
True
>>> a is b
True

如你所见,关于浅拷贝如果元素不可变的还好,没什么副作用;如果元素可变,那就要小心其副作用了。

深拷贝递归拷贝顶层对象以及它内部的子对象,因此,新对象和原来的旧对象,没有任何关联。Python 中使用 copy.deepcopy() 函数来实现对对象的深拷贝。

import copy

a = [1, 'hello', [1,2]]
b = copy.deepcopy(a)

a[0] = 100
a[2][0] = 100
print(a)
print(b)

## 输出结果
[100, 'hello', [100, 2]]
[1, 'hello', [1, 2]]

深拷贝即拷贝了顶层对象,同时还拷贝了子对象,所以 a[2]b[2] 指向的是两个不同的列表。修改 a[2][0] 后,重新指向了新的整数,但是这并不会影响到 b[2]

>>> import copy
>>> a = (1,2,3)
>>> b = copy.deepcopy(a)
>>> a is b
True

>>> a = (1,2,[1,2])
>>> b = copy.deepcopy(a)
>>> a is b
False
>>> a[2][0] = 100
>>> a
(1, 2, [100, 2])
>>> b
(1, 2, [1, 2])

对于不可变对象来说,如果其子对象全部不可变,那么深拷贝就和浅拷贝是一样的效果,都是指向同一个内存地址。

但是如果子对象中包含可变对象,那么深拷贝之后的对象就不再是原来的对象了,因为可变对象被重新拷贝了一份,放到例子中就是 a[2]b[2] 指向的不再是同一个列表。因此,修改 a[2] 并不会影响 b[2]

如果深拷贝的对象中存在指向自身的引用,那么会不会无限递循环呢。

答案是不会,深拷贝函数内部维护了一个字典,该字典记录了已经拷贝的对象与其 id。拷贝过程中,如果字典里已经存储了将要拷贝的对象,则会从字典直接返回,不再进行递归。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我就告诉过你我会飞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值