深入探究Python中变量的拷贝和作用域问题

深入探究Python中变量的拷贝和作用域问题,第1张

这篇文章主要介绍了Python中变量的拷贝和作用域问题,包括一些赋值、引用问题,以及相关函数在Python2和3版本之间的不同,需要的朋友可以参考下

python

中赋值语句总是建立对象的引用值,而不是复制对象。因此,python

变量更像是指针,而不是数据存储区域,

这点和大多数

OO

语言类似吧,比如

C++、java

~

1、先来看个问题吧:

在Python中,令values=[0,1,2];values[1]=values,为何结果是[0,[],2]

1

2

3

4

>>>

values

=

[0,

1,

2]

>>>

values[1]

=

values

>>>

values

[0,

[],

2]

我预想应当是

1

[0,

[0,

1,

2],

2]

但结果却为何要赋值无限次

可以说

Python

没有赋值,只有引用。你这样相当于创建了一个引用自身的结构,所以导致了无限循环。为了理解这个问题,有个基本概念需要搞清楚。

Python

没有「变量」,我们平时所说的变量其实只是「标签」,是引用。

执行

1

values

=

[0,

1,

2]

的时候,Python

做的事情是首先创建一个列表对象

[0,

1,

2],然后给它贴上名为

values

的标签。如果随后又执行

1

values

=

[3,

4,

5]

的话,Python

做的事情是创建另一个列表对象

[3,

4,

5],然后把刚才那张名为

values

的标签从前面的

[0,

1,

2]

对象上撕下来,重新贴到

[3,

4,

5]

这个对象上。

至始至终,并没有一个叫做

values

的列表对象容器存在,Python

也没有把任何对象的值复制进

values

去。过程如图所示:

执行

1

values[1]

=

values

的时候,Python

做的事情则是把

values

这个标签所引用的列表对象的第二个元素指向

values

所引用的列表对象本身。执行完毕后,values

标签还是指向原来那个对象,只不过那个对象的结构发生了变化,从之前的列表

[0,

1,

2]

变成了

[0,

,

2],而这个

则是指向那个对象本身的一个引用。如图所示:

要达到你所需要的效果,即得到

[0,

[0,

1,

2],

2]

这个对象,你不能直接将

values[1]

指向

values

引用的对象本身,而是需要吧

[0,

1,

2]

这个对象「复制」一遍,得到一个新对象,再将

values[1]

指向这个复制后的对象。Python

里面复制对象的 *** 作因对象类型而异,复制列表

values

的 *** 作是

values[:]

#生成对象的拷贝或者是复制序列,不再是引用和共享变量,但此法只能顶层复制

所以你需要执行

1

values[1]

=

values[:]

Python

做的事情是,先

dereference

得到

values

所指向的对象

[0,

1,

2],然后执行

[0,

1,

2][:]

复制 *** 作得到一个新的对象,内容也是

[0,

1,

2],然后将

values

所指向的列表对象的第二个元素指向这个复制二来的列表对象,最终

values

指向的对象是

[0,

[0,

1,

2],

2]。过程如图所示:

往更深处说,values[:]

复制 *** 作是所谓的「浅复制」(shallow

copy),当列表对象有嵌套的时候也会产生出乎意料的错误,比如

1

2

3

4

a

=

[0,

[1,

2],

3]

b

=

a[:]

a[0]

=

8

a[1][1]

=

9

问:此时

a

b

分别是多少

正确答案是

a

[8,

[1,

9],

3],b

[0,

[1,

9],

3]。发现没b

的第二个元素也被改变了。想想是为什么不明白的话看下图

正确的复制嵌套元素的方法是进行「深复制」(deep

copy),方法是

1

2

3

4

5

6

import

copy

a

=

[0,

[1,

2],

3]

b

=

copydeepcopy(a)

a[0]

=

8

a[1][1]

=

9

2、引用

VS

拷贝:

(1)没有限制条件的分片表达式(L[:])能够复制序列,但此法只能浅层复制。

(2)字典

copy

方法,Dcopy()

能够复制字典,但此法只能浅层复制

(3)有些内置函数,例如

list,能够生成拷贝

list(L)

(4)copy

标准库模块能够生成完整拷贝:deepcopy

本质上是递归

copy

(5)对于不可变对象和可变对象来说,浅复制都是复制的引用,只是因为复制不变对象和复制不变对象的引用是等效的(因为对象不可变,当改变时会新建对象重新赋值)。所以看起来浅复制只复制不可变对象(整数,实数,字符串等),对于可变对象,浅复制其实是创建了一个对于该对象的引用,也就是说只是给同一个对象贴上了另一个标签而已。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

L

=

[1,

2,

3]

D

=

{'a':1,

'b':2}

A

=

L[:]

B

=

Dcopy()

print

"L,

D"

print

L,

D

print

"A,

B"

print

A,

B

print

"--------------------"

A[1]

=

'NI'

B['c']

=

'spam'

print

"L,

D"

print

L,

D

print

"A,

B"

print

A,

B

L,

D

[1,

2,

3]

{'a':

1,

'b':

2}

A,

B

[1,

2,

3]

{'a':

1,

'b':

2}

--------------------

L,

D

[1,

2,

3]

{'a':

1,

'b':

2}

A,

B

[1,

'NI',

3]

{'a':

1,

'c':

'spam',

'b':

2}

3、增强赋值以及共享引用:

x

=

x

+

y,x

出现两次,必须执行两次,性能不好,合并必须新建对象

x,然后复制两个列表合并

属于复制/拷贝

x

+=

y,x

只出现一次,也只会计算一次,性能好,不生成新对象,只在内存块末尾增加元素。

x、y

为list时,

+=

会自动调用

extend

方法进行合并运算,in-place

change。

属于共享引用

1

2

3

4

5

6

7

8

9

10

11

12

13

14

L

=

[1,

2]

M

=

L

L

=

L

+

[3,

4]

print

L,

M

print

"-------------------"

L

=

[1,

2]

M

=

L

L

+=

[3,

4]

print

L,

M

[1,

2,

3,

4]

[1,

2]

-------------------

[1,

2,

3,

4]

[1,

2,

3,

4]

4、python

从2x

到3x,语句变函数引发的变量作用域问题

先看段代码:

1

2

3

4

5

6

7

8

9

def

test():

a

=

False

exec

("a

=

True")

print

("a

=

",

a)

test()

b

=

False

exec

("b

=

True")

print

("b

=

",

b)

python

2x

3x

你会发现他们的结果不一样:

1

2

3

4

5

6

7

2x:

a

=

True

b

=

True

3x:

a

=

False

b

=

True

这是为什么呢

因为

3x

exec

由语句变成函数了,而在函数中变量默认都是局部的,也就是说

你所见到的两个

a,是两个不同的变量,分别处于不同的命名空间中,而不会冲突。

具体参考

《learning

python》P331-P332

知道原因了,我们可以这么改改:

1

2

3

4

5

6

7

8

9

10

11

12

def

test():

a

=

False

ldict

=

locals()

exec("a=True",globals(),ldict)

a

=

ldict['a']

print(a)

test()

b

=

False

exec("b

=

True",

globals())

print("b

=

",

b)

这个问题在

stackoverflow

上已经有人问了,而且

python

官方也有人报了

bug。。。

具体链接在下面:

>

1、多线程可以共享全局变量,多进程不能。

2、多线程中,所有子线程的进程号相同;多进程中,不同的子进程进程号不同。

3、线程共享内存空间;进程的内存是独立的。

4、同一个进程的线程之间可以直接交流;两个进程想通信,必须通过一个中间代理来实现。

5、创建新线程很简单;创建新进程需要对其父进程进行一次克隆。

6、一个线程可以控制和 *** 作同一进程里的其他线程;但是进程只能 *** 作子进程。

7、两者最大的不同在于:在多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响;而多线程中,所有变量都由所有线程共享。

更多Python知识,请关注:Python自学网!!

使用Python中的线程模块,能够同时运行程序的不同部分,并简化设计。如果你已经入门Python,并且想用线程来提升程序运行速度的话,希望这篇教程会对你有所帮助。

线程与进程

什么是进程

进程是系统进行资源分配和调度的一个独立单位 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

什么是线程

CPU调度和分派的基本单位 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

进程与线程的关系图

线程与进程的区别:

进程

现实生活中,有很多的场景中的事情是同时进行的,比如开车的时候 手和脚共同来驾驶 汽车 ,比如唱歌跳舞也是同时进行的,再比如边吃饭边打电话;试想如果我们吃饭的时候有一个领导来电,我们肯定是立刻就接听了。但是如果你吃完饭再接听或者回电话,很可能会被开除。

注意:

多任务的概念

什么叫 多任务 呢?简单地说,就是 *** 作系统可以同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。

现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?

答案就是 *** 作系统轮流让各个任务交替执行,任务1执行001秒,切换到任务2,任务2执行001秒,再切换到任务3,执行001秒,这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。

真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以, *** 作系统也会自动把很多任务轮流调度到每个核心上执行。 其实就是CPU执行速度太快啦!以至于我们感受不到在轮流调度。

并行与并发

并行(Parallelism)

并行:指两个或两个以上事件(或线程)在同一时刻发生,是真正意义上的不同事件或线程在同一时刻,在不同CPU资源呢上(多核),同时执行。

特点

并发(Concurrency)

指一个物理CPU(也可以多个物理CPU) 在若干道程序(或线程)之间多路复用,并发性是对有限物理资源强制行使多用户共享以提高效率。

特点

multiprocessProcess模块

process模块是一个创建进程的模块,借助这个模块,就可以完成进程的创建。

语法:Process([group [, target [, name [, args [, kwargs]]]]])

由该类实例化得到的对象,表示一个子进程中的任务(尚未启动)。

注意:1 必须使用关键字方式来指定参数;2 args指定的为传给target函数的位置参数,是一个元祖形式,必须有逗号。

参数介绍:

group:参数未使用,默认值为None。

target:表示调用对象,即子进程要执行的任务。

args:表示调用的位置参数元祖。

kwargs:表示调用对象的字典。如kwargs = {'name':Jack, 'age':18}。

name:子进程名称。

代码:

除了上面这些开启进程的方法之外,还有一种以继承Process的方式开启进程的方式:

通过上面的研究,我们千方百计实现了程序的异步,让多个任务可以同时在几个进程中并发处理,他们之间的运行没有顺序,一旦开启也不受我们控制。尽管并发编程让我们能更加充分的利用IO资源,但是也给我们带来了新的问题。

当多个进程使用同一份数据资源的时候,就会引发数据安全或顺序混乱问题,我们可以考虑加锁,我们以模拟抢票为例,来看看数据安全的重要性。

加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改。加锁牺牲了速度,但是却保证了数据的安全。

因此我们最好找寻一种解决方案能够兼顾:1、效率高(多个进程共享一块内存的数据)2、帮我们处理好锁问题。

mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。队列和管道都是将数据存放于内存中 队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来, 我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性( 后续扩展该内容 )。

线程

Python的threading模块

Python 供了几个用于多线程编程的模块,包括 thread, threading 和 Queue 等。thread 和 threading 模块允许程序员创建和管理线程。thread 模块 供了基本的线程和锁的支持,而 threading 供了更高级别,功能更强的线程管理的功能。Queue 模块允许用户创建一个可以用于多个线程之间 共享数据的队列数据结构。

python创建和执行线程

创建线程代码

1 创建方法一:

2 创建方法二:

进程和线程都是实现多任务的一种方式,例如:在同一台计算机上能同时运行多个QQ(进程),一个QQ可以打开多个聊天窗口(线程)。资源共享:进程不能共享资源,而线程共享所在进程的地址空间和其他资源,同时,线程有自己的栈和栈指针。所以在一个进程内的所有线程共享全局变量,但多线程对全局变量的更改会导致变量值得混乱。

代码演示:

得到的结果是:

首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行(其中的JPython就没有GIL)。

那么CPython实现中的GIL又是什么呢?GIL全称Global Interpreter Lock为了避免误导,我们还是来看一下官方给出的解释:

主要意思为:

因此,解释器实际上被一个全局解释器锁保护着,它确保任何时候都只有一个Python线程执行。在多线程环境中,Python 虚拟机按以下方式执行:

由于GIL的存在,Python的多线程不能称之为严格的多线程。因为 多线程下每个线程在执行的过程中都需要先获取GIL,保证同一时刻只有一个线程在运行。

由于GIL的存在,即使是多线程,事实上同一时刻只能保证一个线程在运行, 既然这样多线程的运行效率不就和单线程一样了吗,那为什么还要使用多线程呢?

由于以前的电脑基本都是单核CPU,多线程和单线程几乎看不出差别,可是由于计算机的迅速发展,现在的电脑几乎都是多核CPU了,最少也是两个核心数的,这时差别就出来了:通过之前的案例我们已经知道,即使在多核CPU中,多线程同一时刻也只有一个线程在运行,这样不仅不能利用多核CPU的优势,反而由于每个线程在多个CPU上是交替执行的,导致在不同CPU上切换时造成资源的浪费,反而会更慢。即原因是一个进程只存在一把gil锁,当在执行多个线程时,内部会争抢gil锁,这会造成当某一个线程没有抢到锁的时候会让cpu等待,进而不能合理利用多核cpu资源。

但是在使用多线程抓取网页内容时,遇到IO阻塞时,正在执行的线程会暂时释放GIL锁,这时其它线程会利用这个空隙时间,执行自己的代码,因此多线程抓取比单线程抓取性能要好,所以我们还是要使用多线程的。

GIL对多线程Python程序的影响

程序的性能受到计算密集型(CPU)的程序限制和I/O密集型的程序限制影响,那什么是计算密集型和I/O密集型程序呢

计算密集型:要进行大量的数值计算,例如进行上亿的数字计算、计算圆周率、对视频进行高清解码等等。这种计算密集型任务虽然也可以用多任务完成,但是花费的主要时间在任务切换的时间,此时CPU执行任务的效率比较低。

IO密集型:涉及到网络请求(timesleep())、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO *** 作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。

当然为了避免GIL对我们程序产生影响,我们也可以使用,线程锁。

Lock&RLock

常用的资源共享锁机制:有Lock、RLock、Semphore、Condition等,简单给大家分享下Lock和RLock。

Lock

特点就是执行速度慢,但是保证了数据的安全性

RLock

使用锁代码 *** 作不当就会产生死锁的情况。

什么是死锁

死锁:当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。即死锁是指多个进程因竞争资源而造成的一种僵局,若无外力作用,这些进程都将无法向前推进。

所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。

死锁代码

python线程间通信

如果各个线程之间各干各的,确实不需要通信,这样的代码也十分的简单。但这一般是不可能的,至少线程要和主线程进行通信,不然计算结果等内容无法取回。而实际情况中要复杂的多,多个线程间需要交换数据,才能得到正确的执行结果。

python中Queue是消息队列,提供线程间通信机制,python3中重名为为queue,queue模块块下提供了几个阻塞队列,这些队列主要用于实现线程通信。

在 queue 模块下主要提供了三个类,分别代表三种队列,它们的主要区别就在于进队列、出队列的不同。

简单代码演示

此时代码会阻塞,因为queue中内容已满,此时可以在第四个queueput('苹果')后面添加timeout,则成为 queueput('苹果',timeout=1)如果等待1秒钟仍然是满的就会抛出异常,可以捕获异常。

同理如果队列是空的,无法获取到内容默认也会阻塞,如果不阻塞可以使用queueget_nowait()。

在掌握了 Queue 阻塞队列的特性之后,在下面程序中就可以利用 Queue 来实现线程通信了。

下面演示一个生产者和一个消费者,当然都可以多个

使用queue模块,可在线程间进行通信,并保证了线程安全。

协程

协程,又称微线程,纤程。英文名Coroutine。

协程是python个中另外一种实现多任务的方式,只不过比线程更小占用更小执行单元(理解为需要的资源)。为啥说它是一个执行单元,因为它自带CPU上下文。这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。

通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定。

在实现多任务时,线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 *** 作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据, *** 作系统还会帮你做这些数据的恢复 *** 作。所以线程的切换非常耗性能。但是协程的切换只是单纯的 *** 作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。

greenlet与gevent

为了更好使用协程来完成多任务,除了使用原生的yield完成模拟协程的工作,其实python还有的greenlet模块和gevent模块,使实现协程变的更加简单高效。

greenlet虽说实现了协程,但需要我们手工切换,太麻烦了,gevent是比greenlet更强大的并且能够自动切换任务的模块。

其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件 *** 作等) *** 作时,比如访问网络,就自动切换到其他的greenlet,等到IO *** 作完成,再在适当的时候切换回来继续执行。

模拟耗时 *** 作:

如果有耗时 *** 作也可以换成,gevent中自己实现的模块,这时候就需要打补丁了。

使用协程完成一个简单的二手房信息的爬虫代码吧!

以下文章来源于Python专栏 ,作者宋宋

文章链接:>

全局变量,是一个相对的概念,对于整个程序而言,有可以在整个程序的任何代码块中都能被访问的变量,被称作全局变量。也有在类中能够被该类的任何代码块都能访问到的变量,也被称作全局变量。所以这里是一个相对的概念。代码定义的fly变量以及构造方法中的long变量都是全局变量,因为在long之前加了一个self的前缀,所有在整个类中,该long变量也是全局变量,至少在该类中的任何地方都可以访问到该变量。全局变量被当做类的一个属性来存储,所以可以说直接通过的访问方式直接访问,访问如下:

class G():

fly = False #类中的全局变量

def __init__(self):

selg_age = 1 #加一个下划线,是一种不成文的规定,意思是该变量是私有变量

selflong = 2 #普通变量

self__width = 3 #有两个下划线,是一种“真”私有变量

def run(self):

r = 4 #局部变量

print("I am running!")

python容器线程安全您需要为将在Python中修改的所有共享变量实现自己的锁定。您不必担心会读取不会被修改的变量(即,并发读取是可以的),因此不可变类型(frozenset,tuple,str)可能是安全的,但这样做不会没受伤对于您将要更改的内容-list,set,dict和大多数其他对象,您应该具有自己的锁定机制(尽管在大多数情况下可以进行就地 *** 作,但线程可能导致到超级讨厌的错误-您最好实施锁定,这很容易)。

以上就是关于深入探究Python中变量的拷贝和作用域问题全部的内容,包括:深入探究Python中变量的拷贝和作用域问题、python多线程和多进程的区别有哪些、一篇文章带你深度解析Python线程和进程等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zz/10126112.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-05
下一篇 2023-05-05

发表评论

登录后才能评论

评论列表(0条)

保存