Coding Python

无尽


  • Home

  • Archives

使用Python几行代码轻松操作多线程和多进程

Posted on 2019-12-02 | In Python进阶 |

多线程和多进程理解

线程:threading
进程:processing
1、threading 更多的是使用在频繁 I/O 的并发场景中,
玩过爬虫的都知道,
我们有时候会通过爬虫下载网页上的图片或其它文件内容,
这时候我们就可以使用 Python 的 threading 模块来做到多线程执行。

2、processing 更多的是针对要充分使用 CPU 的并行场景,
比如的电脑的 CPU 是六核,
你希望你的程序运行能充分利用到六核,
那么就可以使用 processing。

开户 1 个线程去执行 2 的 10 次方

再来看一个爬虫例子

不使用多线程的情况:

可以看到总耗时2.9+秒完成爬取

再来看看开启多线程的情况

实现同样的功能,开启多线程只需0.8223秒就完成了
速度提高了 3.5+ 倍

再来看看开启多进程,
把 ThreadPoolExecutor 改成 ProcessPoolExecutor 就可以了

这里有个问题
多进程情况下,必要要用
if __name__ == ‘__main__:’
的方式才能运行多进程,否则报错
多线程不存在这个情况
这样能运行:

不加 if __name__ == ‘__main__:’ 报错:
提示要加 if __name__ == ‘__main__:’

理解 submit 和 map 两种方式

submit 方法:

map 方法:

它们的区别是:
submit方法 是传入参数从而调用具体方法
源码:

map方法 是传入可迭代对象从而调用具体方法
源码:

多线程和多进程怎么选择的问题

CPU密集型:

可以看到,多线程下执行时间并未缩短,单线程与多线程执行时间基本相同,这是由于GIL会把线程锁住,只允许一个线程执行
实际情况就是多线程至始至终还是单线程执行

  • IO密集型:爬虫等频繁进行 IO 操作使用多线程较好,GIL会在IO等待时间里执行其它线程
  • CPU密集型:对于主要是计算任务的CPU密集型程序使用多进程较好

Python各种下划线的骚操作

Posted on 2019-11-23 | In Python进阶 |

我们在定义一些变量或者方法的时候,
常常会用到下划线,
在 Python 中,下划线可是很有用处的,
比如变量、方法中的使用,
有些是一个下划线开头的:_xx,
有些是两个下划线开头的:__xx,
有些是在名称的结尾添加下划线的:xx_,
还有一些方法我们常常看到两个下划线开头和结尾的:__xx__ 等等…

下划线的运用种类

“_“
“_xx”
“xx_“
“__xx”
“__xx__“
“_classname__xx”

只有一个下划线“_”

主要用在Python解释器中
_ 会指向你最后一次执行的表达式结果
在Python 实时交互的时候常常会用到

指向最后执行的表达式

格式化变量

这样使代码中的变量值更易读
使用在循环中

首尾分别二个下划线 __xx__

这些我们叫它们为“魔法函数”
是Python 内置好了的,直接拿来用就行了
应尽量避免以这种方式命名变量

单个下划线开头的 _xx

以单个下划线开关命名的方法或者变量
就是说明它是仅提供内部使用的
举例说明
先定义好一个模块 test1.py
其中有两个方法,一个是单划线开头,一个不是

接着在第二个模块 test2 中 import test1
调用 test1 中的方法看看
当执行 my_func() 时成功

但是执行 _my_func() 时报错
“_my_func” 没有定义,
因此它只能在模块内部使用

这里有个问题是,直接 import test1
test1._my_func() 也是成功的
把 test1 模块名写在前面相当于显式内部调用吧

单个下划线结尾的 xx_

Python 中的许多关键字
def、return、class、pass、True、False等等
是不能用来命名的

如果非要使用这些系统关键词
就可以在尾部加 _

但不建议这么做,还是选用其它的名称命名吧

两个下划线开头的命名:__xx

这种命名方式更多的是用在类的继承
通过两个下划线开头命名的成员
可以防止被子类重写

这里可以看出
name 可以访问
但是 __age 无法访问的
所以就无法重写父类的 __age

打印对象的属性:

从输出的属性中可以看到
name 还是 name
但 __age 变成了 _Person__age

来来个继承看看

可以看到,name 被子类重写了,而 __age 不能访问,也没有被重写
同时,打印一下 Son 的属性看看

这里 son 类里面定义的 __age 已经变成了 _Son__age
所以要访问或重写只能这样显式调用

可以成功访问父类和子类的__age 属性

一个下划线+类名+二个下划线开头的命名:_classname__xx

可以像这样指定类名的方式命名

Python 并没有向别的编程语言那样有 private 来修饰属性进行私有化,
所以可以通过 __ 的方式进行 “伪私有化”,
不过还是可以通过实例访问得到相应的值

使用enumerate来枚举可遍历对象

Posted on 2019-11-22 | In Python进阶 |

初识枚举函数 enumerate()

今天我们来认识一下 Python 的 enumerate 这个内置的函数,
它就是专门用来枚举可迭代对象的,
通过它我们可以在每次迭代的过程中,
获取到具体的索引、数据和下标。

enumerate 函数语法

enumerate(iterable[, start])

要得到一个 enumerate对象很简单
直接调用 enumerate 方法,把可迭代对象传入就可以了

1
2
list1 = ['hi', 'fxxk', 'Python', 'handsomeb']
print(enumerate(list1))

<enumerate object at 0x02E33850>

当我们执行了 enumerate 方法之后,
它会直接把我们传递进去的可迭代对象
生成含有索引和对应的值的 enumerate 对象,
所以这个对象是可以使用 for 循环的:

1
2
3
4
list1 = ['hi', 'fxxk', 'Python', 'handsomeb']

for item in enumerate(list1):
print(item)

(0, ‘hi’)
(1, ‘fxxk’)
(2, ‘Python’)
(3, ‘handsomeb’)

enumerate 函数可以传两个参数,
一个就是我们刚说的可迭代对象,
另一个就是数字,
这个数字的作用就是让 enumerate 从指定位置(默认为0)开始计数,
也就是说,如果我们希望 enumerate 对我们传进去的可迭代对象从第 5 开始生成,
那么就可以这样:

1
2
3
4
list1 = ['hi', 'fxxk', 'Python', 'handsomeb']

for item in enumerate(list1, 5):
print(item)

(5, ‘hi’)
(6, ‘fxxk’)
(7, ‘Python’)
(8, ‘handsomeb’)

在枚举的时候
也可以将得到的 enumerate 对象的计数单独拿到

1
2
3
4
list1 = ['hi', 'fxxk', 'Python', 'handsomeb']

for count, value in enumerate(list1):
print(count)

0
1
2
3

当然,也可将得到的 enumerate 对象转换成列表

1
2
3
4
list1 = ['hi', 'fxxk', 'Python', 'handsomeb']

list2 = list(enumerate(list1))
print(list2)

[(0, ‘hi’), (1, ‘fxxk’), (2, ‘Python’), (3, ‘handsomeb’)]

那些需要我们做一些计数相关的
或者需要将数字与元素做一些对应关系的时候
就可以使用 enumerate 进行枚举获取,
其内部主要是帮我们做了这样的操作:
enumerate 函数内部实现大概原理如下:

1
2
3
4
5
def enumerate(sequence, start=0):
n = start
for elem in sequence:
yield n, elem
n += 1

简单应用举例

将月份的英语单词与数字月进行对应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
month = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December']

for num, value in enumerate(month, 1):
print(f'{num}月 -》{value}')

1月 -》January
2月 -》February
3月 -》March
4月 -》April
5月 -》May
6月 -》June
7月 -》July
8月 -》August
9月 -》September
10月 -》October
11月 -》November
12月 -》December

以上就是 enumerate 对象的使用方式。

什么是Python的可迭代对象迭代器

Posted on 2019-11-21 | In Python进阶 |

理解可迭代对象

容器: 列表、字典、元组、集合等
都是将数据封装起来也供使用,只是它们各自的“安排方式”不同

容器可以被循环

for item in 容器:

这些可以被 for 循环的容器(对象)
我们就说它是可迭代的,
也就是可迭代对象,是 Iterable 的

isinstance() 函数来判断一个对象是否是一个已知的类型
可以通过 isinstance 来判断这个对象是否可迭代(Iterable)

1
2
3
4
5
6
7
8
from collections import Iterable

print('列表', isinstance([], Iterable))
print('字典', isinstance({}, Iterable))
print('元组', isinstance((), Iterable))
print('字符串', isinstance('', Iterable))
print('整数', isinstance(66, Iterable))
print('生成器', isinstance((i for i in range(10)), Iterable))

列表 True
字典 True
元组 True
字符串 True
整数 False
生成器 True

可以看到,除了整型对象外,其余对象、生成器均是可迭代对象

这些可迭代对象(Iterable)可以生成迭代器(Iterator)

迭代器的特点

当我们去使用迭代器的时候,
我们不用关心这个对象的长度,
不用关心容器里面是如何安排数据的。

迭代器之所以可迭代,
是因为它们提供了一个 next() 方法,
通过这个方法我们可以不断的去获取容器里面的下一个元素,
直到这个容器没有数据然后报 StopIteration 为止:

1
2
list1 = [11, 22, 33, 44, 212]
print(next(list1))

TypeError: ‘list’ object is not an iterator

这里列表是可迭代对象(Iterable),但还不是迭代器,因此不能使用 next() 方法
将列表转为迭代器(Iterator)要使用iter 魔法

1
2
3
4
5
6
7
list1 = [11, 22, 33, 44, 212].__iter__()
print(next(list1))
print(next(list1))
print(next(list1))
print(next(list1))
print(next(list1))
print(next(list1))

22
33
44
212
StopIteration

可以看到,我们这里调用 iter 魔法方法,
成功将列表(可迭代对象)变为 迭代器(Iterator), 从而使用 next 方法

可以通过 isinstance 来判断这个对象是否是迭代器(Iterator)

1
2
3
4
5
6
7
from collections import Iterator
print('列表', isinstance(iter([]), Iterator))
print('字典', isinstance(iter({}), Iterator))
print('元组', isinstance(iter(()), Iterator))
print('字符串', isinstance(iter(''), Iterator))
print('整数', isinstance(66, Iterator))
print('生成器', isinstance((i for i in range(10)), Iterator))

列表 True
字典 True
元组 True
字符串 True
整数 False
生成器 True

从以上输出结果可以看出
生成器本身也是迭代器(iterator)
所以不用 iter 魔法转换,
生成器继可以用 next 不断迭代,也可以使用 for 循环。

只有可迭代对象才能用 iter 魔法转换为 迭代器(iterator)
所以下列代码中 iter(66)报措

1
print('整数', isinstance(iter(66), Iterator))

TypeError: ‘int’ object is not iterable

总结
到这里你应该明白这么几点:

1、迭代和我们一般所认为的遍历枚举的差别在于:
迭代是不需要知道容器的“容量”的,
每次访问都可以获取到下一个元素,直到没有为止。
就好像你去 Apple 官网买 iPhone 一样,
你不需要他们罗列出了多少台手机,
你可以一直买,
每次他都给你发手机,
直到没货了,
他才会跟你说不好意思,库存不足。(StopIteration)停止迭代

2、可迭代对象不可以使用 next() 方法,迭代器才可以,
如何成为迭代器呢?使用 iter 方法。

3、迭代器是在每次获取才去计算下一个元素的,
所以它降低不必要的内存浪费,也可以存储一些 “理想” 的数据,
比如可以存储根据一定规律的无穷个数字:“2,4,6,8,10…….10000000000000(无穷)”,
但是用一般的列表来存储的话,根本不可能,内存早就爆炸了。

4、 for i in 迭代器的过程中,实际上就是再不断的调用 next():

1
2
3
4
5
list1 = iter(range(10))
print(list1)

for item in list1:
print(item)

<range_iterator object at 0x0293DCF8>
0
1
2
3
4
5
6
7
8
9

等价于:

1
2
3
4
5
6
7
8
list1 = iter(range(10))
print(list1)

while True:
try:
print(next(list1))
except StopIteration: # 没有数据了
break

<range_iterator object at 0x0263DCF8>
0
1
2
3
4
5
6
7
8
9

5、生成器是一个迭代器。

yield是什么魔鬼生成器

Posted on 2019-11-19 | In Python进阶 |

生成器的理解

生成器与列表相关
我们知道一行代码就可以生成一个列表 [x for x in range(num)]

1
2
my_list = [[x, x % 2] for x in range(10)]
print(my_list)

[[0, 0], [1, 1], [2, 0], [3, 1], [4, 0], [5, 1]]

1
2
my_list = ([x, x % 2] for x in range(6))
print(my_list)

<generator object at 0x069B8970>

第二个代码输出了一个 generator(生成器) 对象
区别是:
第一个代码用的[]
第二个代码用的()
这就是生成器,不直接输出列表
可以通过 next() 方法调用这个生成器里的元素

1
2
3
4
5
6
7
print(next(my_list))
print(next(my_list))
print(next(my_list))
print(next(my_list))
print(next(my_list))
print(next(my_list))
print(next(my_list))

[0, 0]
[1, 1]
[2, 0]
[3, 1]
[4, 0]
[5, 1]

这有什么区别呢?
现在把运算加大,
考虑这两行代码的运行时间,谁长谁短呢?
来个装饰器计时,看看两者的区别

1
2
my_list1 = [[x, x % 2] for x in range(6666666)]
my_list2 = ([x, x % 2] for x in range(6666666))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time

def log_func_time(func):
def wrapper():
start_time = time.perf_counter()
result = func()
end_time = time.perf_counter()
print('方法 {} , 消耗了 {} ms'.format(func.__name__, (end_time - start_time) * 1000))
return result

return wrapper

@log_func_time
def calculate_func_1():
my_list1 = [[x, x % 2] for x in range(6666666)]

@log_func_time
def calculate_func_2():
my_list2 = ([x, x % 2] for x in range(6666666))

calculate_func_1()
calculate_func_2()

方法 calculate_func_1 , 消耗了 3356.3896 ms
方法 calculate_func_2 , 消耗了 0.0181000000001319 ms

从返回结果看出,方法二,也就是生成器所消耗的时间远小于方法一
这是因为
方法一:直接生成了 6666666 个元素并封闭进列表,耗时并占用大量内存
方法二:只记录了设定的算法规则,这里的 for x in range(6666666) 就是不断 +1 的规则
生成器只需记住这个规则,不需要一次性地将列表封装,
等在需要的时候,再按规则运算一下,然后将结果返回
几乎不占内存

总结,这就是生成器
它是根据一定的规律算法生成的,
当我们去遍历它的时候,
它可以通过特定的算法不断的推算出相应的元素,
边运行边推算结果,从而节省了很多空间。

继续体会这两者的区别
方法1将结果直接存入列表,for 循环只是将现存列表简单遍历打印出来
方法2是需要的时候才运算提取,每次 for 循环调用一次规则:([x, x % 2] for x in range(15))

1
2
3
4
5
6
7
8
9
10
11
@log_func_time
def calculate_func_1():
my_list1 = [[x, x % 2] for x in range(15)]
for item in my_list1:
print(item)

@log_func_time
def calculate_func_2():
my_list2 = ([x, x % 2] for x in range(15))
for item in my_list2:
print(item)

魔法关键词 yield

yield 与 return 关键词都是运用于函数中返回值
return 在函数中执行这个关键词就会返回相应的结果终止函数的运行

1
2
3
4
5
6
7
def foo():
a = 1
b = 2
return a + b
print("ooxx-ooxx-oooooxxxxx.....")

print(foo())

3

函数遇到 return 关键字就返回,而后的语句不再被执行到

再来看 yield 关键字

1
2
3
4
5
6
7
8
9
10
11
12
def foo():
print('1111')
yield
print('2222')
yield
print('3333')
yield
print('4444')
yield

f = foo()
print(type(f))

<class ‘generator’>

直接调用生成器函数没有返回结果
打印出对象类型为 generator(生成器)

一个函数里定义了 yield 关键字,那么这个函数就是一个生成器

可以用 next 方法使用生成器

1
2
3
4
5
next(f)
next(f)
next(f)
next(f)
next(f)

1111
2222
3333
4444
StopIteration
前四个 next 依次返回打印
第五个 next 没有数据了,返回 StopIteration

再遍历一下看看:

1
2
for item in f:
print(item)

1111
None
2222
None
3333
None
4444
None

返回了 None, 这是因为 yield 行并没有具体返回结果

再来看 yield 后加上返回内容的情形:

1
2
3
4
5
6
7
8
9
10
def foo():
yield '第1次 yield'
yield '第2次 yield'
yield '第3次 yield'
yield '第4次 yield'

f = foo()

for item in f:
print(item)

第1次 yield
第2次 yield
第3次 yield
第4次 yield

以上仅仅展示简单的示例,也求快速理解 yield
生成器是按一定的规律算法生成的列表
当我们去遍历它的时候,它可以通过特定的算法不断的推算出相应的元素,
边运行边推算结果,从而节省了很多空间。

所以 yield 一般是用在 for 循环中以一定规则生成
简单运用举例:输出18以内的偶数

1
2
3
4
5
6
def foo():
for num in range(10):
yield num * 2

for item in foo():
print(item)

0
2
4
6
8
10
12
14
16
18

另外,yield 后面可以跟常用的数据类型,
比如:string、int、dict 等
比如:

1
2
3
4
5
yield {
'text': text,
'class_attrib': format_html(' class="column-{}', field_name),
'sortable': False
}

再比如 Scrapy 爬虫框架中经常要 yield item

1
2
3
4
for img in response.xpath('//img[@referrerpolicy]'):
item['alt'] = img.xpath('./@alt').extract()
item['data_original'] = img.xpath('./@data-original').extract()
yield item

如何一行代码就快速生成列表

Posted on 2019-11-18 | In Python进阶 |

牛逼的一行代码生成列表
将数据封装到列表,常用的方法多半如此:
需要多行代码

1
2
3
4
5
6
7
8
9
10
11
12
13
my_list = []
my_list2 = []

for i in range(1, 15):
if i % 2 == 0:
my_list.append(i)

for item in my_list:
item = item * 2
my_list2.append(item)

print(my_list)
print(my_list2)

[2, 4, 6, 8, 10, 12, 14]
[4, 8, 12, 16, 20, 24, 28]

人生苦短
对于一些通过循环和特定条件生成的列表
更好的解决方法是用一行代码制定它们
一行代码搞定15以内偶数封装:

1
2
my_list = [i for i in range(1, 15) if i % 2 == 0]
print(my_list)

[2, 4, 6, 8, 10, 12, 14]

一行代码将15以内的偶数 * 2,封装

1
2
my_list2 = [i * 2 for i in range(1, 15) if i % 2 == 0]
print(my_list2)

[4, 8, 12, 16, 20, 24, 28]

总结:
基本形式:
new_list = [表达式 循环语句 条件]
解释意思:
位置1放:表达式
位置2放:循环语句
位置3放:判断条件
就这么简单粗暴

这里的 表达式 不仅仅可以做加减乘除,也可以放一些方法调用,
例如将列表中的单词全部转换为小写:

1
2
3
my_list3 = ['WHAT', 'THE', 'FUCK', 'PYTHON', 'HANDSOMEB', 'NICE']
new_list = [s.lower() for s in my_list3]
print(new_list)

[‘what’, ‘the’, ‘fuck’, ‘python’, ‘handsomeb’, ‘nice’]

生成多个元组,再将多个元组放入列表中

1
2
3
my_list4 = [2, 3, 4, 5, 6, 7, 8, 9, 0]
new_my_list4 = [(i, i**2) for i in my_list4]
print(new_my_list4)

[(2, 4), (3, 9), (4, 16), (5, 25), (6, 36), (7, 49), (8, 64), (9, 81), (0, 0)]

再来看一个 for 循环的嵌套例子
将两个列表中相同的元素放入新列表
传统方法这样写:

1
2
3
4
5
6
7
8
9
10
my_list = [1, 2, 4, 5, 6, 7, 8, 9]
my_list2 = [9, 2, 3, 4, 8]
new_list = []

for i in my_list:
for j in my_list2:
if i == j:
new_list.append(i)

print(new_list)

[2, 4, 8, 9]

改为一行代码搞定:

1
2
new_list = [i for i in my_list for j in my_list2 if i == j]
print(new_list)

[2, 4, 8, 9]

new_list = [表达式 循环语句 条件]

python中的函数高级用法装饰器语法糖

Posted on 2019-11-14 | In Python进阶 |

函数可以被当做参数进行调用

装饰器的两种写法

第一种:方便理解

1
2
3
4
5
6
7
8
9
10
11
def func_1(func):
def func_2():
print('func_2')
func()
return func_2

def say():
print('fxxk python')

f = func_1(say)
f()

func_2
fxxk python

1、一开始通过执行 f = func_1(say),也就是说,把 say 这个方法传递给 func_1 函数;

2、那么这时候就会执行第二行,遇到了 func_2 函数然后 return 回来;

3、f 指向了这个 func_1 ;

4、接着执行 f() , 这时候就会执行第三行打印 fuc_2;

5、接着会向下执行 func(), 进入第 8 行打印 fxxk python。

你仔细看这个 func_1 函数,它的参数是接收一个函数,然后在内部使用它,最后返回的是另一个函数。

也就是说,一个函数,把另一个函数作为参数传进来,然后在内部去使用这个被传递进来的函数,
最后改变了这个函数的返回结果。我们就叫这个函数为装饰器。

所以,我们刚刚举得例子中, func_1 就是装饰器。

第二种 装饰器的最佳写法@

1
2
3
4
5
6
7
8
9
10
11
def func_1(func):
def func_2():
print('func_2')
func()
return func_2

@func_1
def say():
print('fxxk python')

say()

func_2
fxxk python

输出结果一样,这两段代码的意思相同,只是写法不同,而其中 @func_1 就是语法糖的写法
通过 @ + 函数名称,就可以实现直接将 say 函数传递给 func_1,如此就是装饰器的最佳写法

如何给装饰器传递参数

不明确指定参数

通过 args,*kwargs 接收任意参数

1
2
3
4
5
6
7
8
9
10
11
def func_1(func):
def func_2(*args, **kwargs):
print('func_2')
func(*args, **kwargs)
return func_2

@func_1
def say(something):
print(something)

say('fxxk python')

func_2
fxxk python

也可以明确指定参数

1
2
3
4
5
6
7
8
9
10
11
def func_1(func):
def func_2(ss):
print('func_2')
func(ss)
return func_2

@func_1
def say(something):
print(something)

say('fxxk python')

func_2
fxxk python

多个装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def func_1(func):
def func_inner(*args, **kwargs):
print('func_1')
func(*args, **kwargs)
return func_inner

def func_2(func):
def func_inner(*args, **kwargs):
print('func_2')
func(*args, **kwargs)
return func_inner

@func_1
@func_2
def say(something):
print(something)

say('fxxk python')

func_1
func_2
fxxk python

以上就是装饰器的用法,
它可以将逻辑代码和业务分离开来,
降低代码的耦合度,提高代码的使用率。
比如记录一些代码的 log ,检查用户的合法性,都可以使用装饰器来高效使用。

用户验证场景的装饰器使用

装饰器 user_check 实现用户合法性校验逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
def user_check(func):
def wrapper(username, password):
if (username != 'root'):
raise Exception('permission denied')
elif (password != '1234'):
raise Exception('password incorrect')
else:
return func(username, password)
return wrapper

@user_check
def login_user(username, password):
print('login success')
1
login_user('name', '1234')

用户名不是 root ,抛出异常

raise Exception(‘permission denied’)
Exception: permission denied

1
login_user('root', '1111')

密码不是 1234 ,抛出异常

raise Exception(‘password incorrect’)
Exception: password incorrect

用户合法

1
login_user('root', '1234')

login success!

只要别的地方需要验证用户合法性,就直接去引用这个装饰器 user_check 就可以了

以上就是装饰器的用法,
它可以将逻辑代码和业务分离开来,
降低代码的耦合度,提高代码的使用率。
比如记录一些代码的 log ,检查用户的合法性,都可以使用装饰器来高效使用。

python函数中参数的传递机制疑惑理解

Posted on 2019-11-12 | In Python进阶 |

代码1

1
2
3
4
5
6
def foo(arg):
arg += 1

a = 2
foo(a)
print(a)

2

代码2

1
2
3
4
5
6
def foo(arg):
arg.append(6)

a = [1, 2, 3]
foo(a)
print(a)

[1, 2, 3, 6]

可以看出,
在执行完 foo 函数后
代码1 变量 a 值没有变化,还是 2
代码2 变量 a 值发生了变化,变为[1, 2, 3, 6]

为什么会这样呢?
还是重点Python中变量叫“名称”,赋值叫“贴标签”

结合python中一切皆对象(object)的特点
a = 2, 实际是将整数对象 2 贴上了 a 这个标签
a = [1, 2, 3] ,实际是将列表对象 [1, 2, 3] 贴上了 a 这个标签

现在再来分析这两个代码
代码1:
一开始,a=2 将整数对象2贴上标签a
接着执行 foo 函数,将整数对象2贴上标签arg
此时标签a、arg均指向整数对象2
函数执行 arg += 1 后
arg标签指向整数对象 3
而 a 标签还是指向整数对象2
这时候再执行 print(a),当然输出为 2

代码2:
当 a = [1, 2, 3] 时,标签 a 指向列表对象[1, 2, 3]
把 a 传递给 foo 函数时,arg 标签同样指向列表对象[1, 2, 3]
执行 append(6) 时,标签 a 和 arg 同时指向列表对象[1, 2, 3, 6]
所以最后打印结果为 [1, 2, 3, 6]

原因分析:
整数型 和 列表 的数据类型不同,
整数型对象是不可变类型,而列表是可变类型
当整数型 arg += 1 时,要保持原 a = 2 内存地址不变,系统会开辟一个新的空间来创造新的内存地址给引用arg
而列表对象在 append 时,列表天生就是可变的,所以不会创建新的内存地址
注意仅仅限于 append 方法, 如果用 += 方法也是会创建新的空间的
列表在使用 append方法进行改变时只是在当前做改变,所以代码中标签 a、arg 它们均指向了同一个内存地址

总结
Python中的–赋值传递,它的关键之处在于数据类型是否可变
整数、元组、字符串是不可变的,所以它们作为参数进行函数操作时,会创建新的内存地址以供引用
列表、字典是可变的,作为参数进行函数操作时,不会创建新的地址

所以,当不可变类型变量希望通过函数进行“数据改变”时,
代码1可以如此这般,重新贴一次标签

1
2
3
4
5
6
def foo(arg):
return arg + 1

a = 2
a = foo(a) # 重新贴一次标签
print(a)

3

注意:可变类型进行 += 操作时候,也是会创建新的内存空间的

1
2
3
4
5
6
def foo(arg):
return arg + [6]

a = [1, 2, 3]
foo(a)
print(a)

[1, 2, 3]

1
2
3
4
5
6
def foo(arg):
return arg + [6]

a = [1, 2, 3]
a = foo(a)
print(a)

[1, 2, 3, 6]

进一步结论,变量在进行函数操作后,值是否改变与这些有关
其一是:
变量类型是不是可变的
可变:列表、字典
不可变:整型、元组、字符串
其二是:
方法不同
+= 方法,不变,需要新开辟空间
append 方法,可变,不用新开空间,原基础上改变

如何更好地使用print

Posted on 2019-11-08 | In Python入门 |

print默认是自动换行的
print的结尾默认加了一个换行转义字符 \n,所以会默认换行的

print()
print()

print 使用 end 参数

1
print(end='。')

。

1
2
3
print('乾-大象')
print('天行健', end=',')
print('君子以自强不息', end='。')

乾-大象
天行健,君子以自强不息。
Process finished with exit code 0

使用 sep

作用是将多个字符串 串起来打印,字符串之间加标点符号
不带 sep 默认是字符串之间加空格

1
2
print('天行健', '君子以自强不息')
print('天行健', '君子以自强不息', sep=',')

天行健 君子以自强不息
天行健,君子以自强不息

巧用 format 和 f-string

1
2
print('小明今年' + str(18))
print('小明今年{}'.format(18))

小明今年18
小明今年18

或者这样
f-string 方式更灵活
直接在字符串中添加{},在{}中添加变量或函数都可以

1
2
age = 18
print(f'小明今年{age}')

小明今年18

在{}中添加函数

1
2
3
4
def test():
return 17

print(f'小明今年{test()}')

小明今年17

print 的另一种格式化 %

最常用的点位符是 %s 和 %d
注意 % 前后不加逗号,
用元组小括号()将要打印的数据包裹起来即可

1
print('%s今年%d,长度是%d' % ('小明', 18, 18))

小明今年18,长度是18

print 还可以打印各种类型

列表、元组、变量都不在话下

1
2
3
4
5
6
list = [1, 2, 3, 4, 5, 6]
tuple = (1, 2, 3, 4, 5, 6)
num = 999
print(list)
print(tuple)
print(num)

[1, 2, 3, 4, 5, 6]
(1, 2, 3, 4, 5, 6)
999

打印类实例

1
2
3
4
5
6
class Person():
def __init__(self, name, length):
self.name = name
self.length = length

print(Person('小明', 18))

<main.Person object at 0x0119DDF0>

1
2
3
4
5
6
7
8
9
class Person():
def __init__(self, name, length):
self.name = name
self.length = length

def __str__(self):
return f'Person的信息是 name:{self.name} length:{self.length}'

print(Person('小明', 18))

Person的信息是 name:小明 length:18

以上代码先在类中定义了 str 方法,这样在执行 print 的时候
它会先去执行 str 方法,从而打印出相关信息

如何理解Python中的闭包

Posted on 2019-11-07 | In Python进阶 |

闭包的理解基础

闭包解决的是函数嵌套的变量访问问题

非闭包场景

内部函数 inner_fun 可以成功访问外部变量 x

1
2
3
4
5
6
def outer_fun(x):
def inner_fun():
print(x)
inner_fun()

outer_fun(6)

6

如果在内部函数 inner_fun 要修改变量 x 的值,就行不通了

1
2
3
4
5
6
7
def outer_fun(x):
def inner_fun():
x += 1
print(x)
inner_fun()

outer_fun(6)

x += 1
UnboundLocalError: local variable ‘x’ referenced before assignment

这时想修改必须 在内部函数 inner_fun 中声明 nonlocal 变量才行

1
2
3
4
5
6
7
8
def outer_fun(x):
def inner_fun():
nonlocal x
x += 1
print(x)
inner_fun()

outer_fun(6)

7

闭包场景

闭包举例
这里将外部函数 outer_fun 的返回,return 为这个嵌套函数 inner_fun
outer_fun(6) 调用了第一次后,x + 1 = 7
再次调用 outer() 时,记住了这个变量 7

1
2
3
4
5
6
7
def outer_fun(x):
def inner_fun():
print(x + 1)
return inner_fun

outer = outer_fun(6)
outer()

7

注意看 outer = outer_fun(6) 这一行代码,是不是跟类的实例化非常相似呢?
这就是闭包,可以将一些变量数据直接附加到“闭包”里面,
此时这个“闭包”会记住这些变量数据。
好比面向对象的封装一样,把一些属性和变量封装到一个对象类里面

怎么创建一个闭包

首先它得是一个嵌套函数
接着内部函数引用nonlocal变量
最后嵌套函数的返回是内部函数

闭包具体应用简单举例

1
2
3
4
5
6
7
def outer_fun(x):
def inner_fun(y):
print(x + y)
return inner_fun

outer = outer_fun(5)
outer()

TypeError: inner_fun() missing 1 required positional argument: ‘y’

这里会报错,因为内部函数 inner_fun 也要传入一个参数 y

1
2
3
4
5
6
7
8
9
def outer_fun(x):
def inner_fun(y):
print(x + y)
return inner_fun

outer = outer_fun(5)
outer(1)
outer(6)
outer(60)

6
11
65

outer = outer_fun(5) 引用记住了参数 5
outer(1) outer(6) outer(60) 依次执行 + y 的操作

可以看到,当我们使用闭包的时候,
我们不需要定义全局变量,也可以访问到函数内部的数据
(一般情况下函数外部是没有办法访问函数内部的变量数据的),
也就是说可以拿到闭包提供的隐藏数据。

注意看 outer = outer_fun(5) 这一行代码,是不是跟类的实例化非常相似呢?
这样的话,如果我们的某个功能只需要一个函数的话,那么我们就直接写一个闭包,而不用写一个类那么麻烦。

我想,往后我跟你说装饰器的时候,你会更加明白闭包的应用。

总之,Python 中的闭包就像一个包,
这个包的牛逼之处在于,
它可以让我们引用函数中的 nonlocal 变量,
这个包可以隐藏一些变量数据,当我们需要的时候,就可以从这个包获取。

当你发现你要写的类只需要一个方法就能实现的时候,用闭包是一种更好的选择。

12345

LST

Python编程学习, 程序化思维提升, 总结

43 posts
7 categories
222 tags
© 2020 LST
Powered by Hexo
|
Theme — NexT.Muse v5.1.4