迅速提高你的Python:理解Python的执行模型

Python 新手通常对他们自己的代码感到惊讶。他们期望 A ,但看起来没有原因,发生了 B 。许多这些“惊奇”的根本原因是混淆了 Python 执行模型。它这样的,如果向你解释一次,一些 Python 概念在变得清晰之前,看起来是模糊不清的。仅仅靠你自己去 “ 弄清楚 ” 也是很困难的,因为它要求对核心语言概念,如变量、对象及函数的思考有根本性的转变。

在本文,我将帮助你理解,在创建变量或调用函数等常见操作背后发生看什么。因此,你将编写更清晰、更易于理解的代码。你还成为一个更好(更快)的代码读者。所需要的就是忘记你所知道的关于编程的一切……

一切都是一个对象?

在大多数人第一次听到在 Python 里“一切都是一个对象”时,这触发了对 Java 等语言的记忆重现,其中用户编写的每一件东西都封装在一个对象里。其他人假设这意味着在 Python 解释器的实现中,一切都实现为对象。第一个解释的错误的;第二个成立,但不是特别有趣(对我们的目的)。这个短语实际指的是所有“事物”这个事实,不管它们是值、类、函数、对象实例(显然)以及几乎其他语言构造,概念上是一个对象。

一切都是对象意味着什么?它意味着提到的“事物”有我们通常与对象联系起来的所有属性(以面向对象的观念);类型有成员函数函数属性( attribute ),模块可以作为实参传递等。它对 Python 中的赋值如何工作具有重要的意义

Python 解释器通常搞混初学者的一个特性是,在一个赋值给一个用户定义对象的“变量”上调用 print() 时会发生什么(稍后我会解释引号)。使用内置类型,通常打印出一个正确的值,像在 string 及 int 上调用 print() 时。但于简单的用户定义类,解释器吐出一些难看的字符串,像:

>>> class Foo (): pass

>>> foo = Foo()

>>> print (foo)

< __main__ . Foo object at 0xd3adb33f>

Print() 是假设打印出一个 “ 变量的值的,对吗?那么为什么它打印出这些垃圾

回答是,我们需要理解 foo 实际上在 Python 里代表什么。大多数其他语言称它为变量。实际上,许多 Python 文章把 foo 称为一个变量,但实际上仅作为一个速记法。

在像 C 的语言里, foo 代表 “ 东西”所用的储存。如果我们写

int foo = 42 ;

说整形变量 foo 包含值 42 是正确的。即, 变量是值的一种容器 。

现在来看一些完全不同的东西

在 Python 中,不是这样的。在我们这样声明时:

>>> foo = Foo()

说 foo“ 包含 ” 一个 Foo 对象是错误的。相反, foo 是绑定到由 Foo() 创建的对象的名字。等式右手侧部分创建了一个对象。把 foo 赋值为这个对象只是说“我希望能够把这个对象称作 foo ”。替代(在传统意义上的)变量, Python 有名字( name )与绑定( binding )。

因此,在之前我们打印 foo 时,解释器展示给我们的是内存中 foo 绑定的对象储存的地址。这不像它听起来那么无用。如果你在解释器中,并希望查看两个名字是否绑定到相同的对象,通过打印它们、比较地址,你可以进行一次权宜的检查。如果它们匹配,它们绑定到同一个对象;如果不匹配,它们绑定到不同的对象。当然,检查两个名字是否绑定到同一个对象惯用的方法是使用 is

如果我们继续我们的例子并写出

>>> baz = foo

我们应该把这读作“将名字 baz 绑定到 foo 所绑定的相同对象(不管是什么)。”这应该是清楚的,那么为什么会发生下面的情况

>>> baz . some_attribute

Traceback (most recent call last):

File "",line 1, in < module >

AttributeError : 'Foo' object has no attribute 'some_attribute'

>>> foo . some_attribute = 'set from foo'

>>> baz . some_attribute

'set from foo'

使用 foo 以某种发生改变对象也将反映到 baz 里:它们都绑定到底下相同的对象。

名字里有什么……

Python 里的名字并非不像真实世界中的名字。如果我妻子叫我“ Jeff ”,我爸爸叫我“ Jeffrey ”,而我老板叫我“编程队长”,很好,但它没有改变我任何一点。不过,如果我妻子杀死了“ Jeff ”(以及埋怨她的人),意味着“编程队长”也被杀死了。类似的,在 Python 中将一个名字绑定一个对象不改变它。不过,改变该对象的某个属性,将反映在绑定到该对象的所有其他名字里。

一切确实是对象。我发誓

这里,提出了一个问题:我们怎么知道等号右手侧的东西总是一个我们可以绑定一个名字的对象?下面怎么样

>>> foo = 10

或者

>>> foo = "Hello World!"

现在是“一切都是对象”回报的时候了。在 Python 里,任何你可以放在等号右手侧的东西是(或创建了)一个对象。 10 与 Hello World 都是对象。不相信我?你自己看

>>> foo = 10

>>> print (foo . __add__)

< method - wrapper '__add__' of int object at 0x8502c0>

如果 10 实际上只是数字 10 ,它不可能有一个 __add__ 属性(或者其他任何属性)。

实际上,使用 dir() 函数,我们可以看到 10 的所有属性

>>> dir ( 10 )

[ '__abs__','__add__','__and__','__class__','__cmp__','__coerce__','__delattr__',

'__div__','__divmod__','__doc__','__float__','__floordiv__','__format__',

'__getattribute__','__getnewargs__','__hash__','__hex__','__index__',

'__init__','__int__','__invert__','__long__','__lshift__','__mod__',

'__mul__','__neg__','__new__','__nonzero__','__oct__','__or__',

'__pos__','__pow__','__radd__','__rand__','__rdiv__','__rdivmod__',

'__reduce__','__reduce_ex__','__repr__','__rfloordiv__','__rlshift__',

'__rmod__','__rmul__','__ror__','__rpow__','__rrshift__','__rshift__',

'__rsub__','__rtruediv__','__rxor__','__setattr__','__sizeof__','__str__',

'__sub__','__subclasshook__','__truediv__','__trunc__','__xor__',

'bit_length','conjugate','denominator','imag','numerator','real' ]

带有所有这些属性与成员函数,我觉得说 10 是一个对象是安全的。

因为 Python 里一切本质上是绑定到对象的名字,我们可以做像这样(有趣)的蠢事:

>>> import datetime

>>> import imp

>>> datetime . datetime . Now()

datetime . datetime( 2013,02,14,53,59,608842 )

>>> class PartyTime ():

... def __call__ ( self,* args):

... imp . reload(datetime)

... value = datetime . datetime( * args)

... datetime . datetime = self

... return value

...

... def __getattr__ ( self,value):

... if value == 'Now' :

... return lambda print ( 'Party Time!' )

... else :

... imp . reload(datetime)

... value = getattr (datetime . datetime,value)

... datetime . datetime = self

... return value

>>> datetime . datetime = PartyTime()

>>> datetime . datetime . Now()

Party Time!

>>> today = datetime . datetime( 2013,2,14 )

>>> print (today)

2013-02-14 00 : 00 : 00

>>> print (today . timestamp())

1360818000.0

Datetime.datetime 只是一个名字(恰好绑定到表示 datetime 类的一个对象)。我们可以随心重新绑定它。在上面的例子中,我们将 datetime 的 datetime 属性绑定到我们的新类, PartyTime 。任何对 datetime.datetime 构造函数调用返回一个有效的 datetime 对象。实际上,这个类与真实的 datetime.datetime 类没有区别。即,除了如果你调用 datetime.datetime.Now() 它总是打印 ’Party Time!’ 这个事实。

显然,这是一个愚蠢的例子,但希望它能给予你某些洞察,在你完全理解并使用 Python 的执行模型时,什么是可能的。不过,现在我们仅改变了与一个名字关联的绑定。改变对象本身会怎么样?

对象的两个类型

事实证明 Python 有两种对象:可变( mutable )与不可变( Immutable )。可变对象的值在创建后可以改变。不可变对象的值不能。 List 是可变对象。你可以创建一个列表,添加一些值,这个列表就地更新。 String 是不可变的。一旦你创建一个字符串,你不能改变它的值。

我知道你的想法:“当然,你可以改变一个字符串的值,我在代码里总是这样做!”在你“改变”一个字符串时,你实际上把它重新绑定到一个新创建的字符串对象。原来的对象维持不变,即使可能没人再引用它了。

你自己看:

>>> a = 'foo'

>>> a

'foo'

>>> b = a

>>> a += 'bar'

>>> a

'foobar'

>>> b

'foo'

即使我们使用 += ,并且看起来我们修改了这个字符串,我们实际上只是得到了包含改变结果的新字符串。这是为什么你可能听到别人说,“字符串串接是慢的。”这是因为串接字符串必须为新字符串分配内存并拷贝内容,而附加到一个 list (在大多数情形里)不要求内存分配。不可变对象“改变”本质上代价高昂,因为这样做设计创建一个拷贝。改变可变对象是廉价的。

不可变对象的离奇性

在我说不可变对象的值在创建后不能改变时,这不是全部的事实。 Python 里的如果容器,比如 tuple ,是不可变的。一个 tuple 的值在它创建后不能改变。但 tuple 的“值”概念上只是一系列到对象绑定不可变的名字。要注意的关键是绑定是不可变的,不是它们绑定的对象。

这意味着下面是完全合法的:

>>> class Foo ():

... def __init__ ( self ):

... self . value = 0

... def __str__ ( self ):

... return str ( self . value)

... def __repr__ ( self ):

... return str ( self . value)

...

>>> f = Foo()

>>> print (f)

0

>>> foo_tuple = (f,f)

>>> print (foo_tuple)

( 0,0 )

>>> foo_tuple[ 0 ] = 100

Traceback (most recent call last):

File "", in < module >

TypeError : 'tuple' object does not support item assignment

>>> f . value = 999

>>> print (f)

999

>>> print (foo_tuple)

( 999,999 )

当我们尝试直接改变这个元组一个元素时,我们得到一个 TypeError ,告诉我们(一旦创建), tuple 就不可赋值。但改变底下的对象具有“改变”该 tuple 值的效果。这是一个难以理解的要点,但无疑是重要的:一个不可变对象的“值”不能改变,但它的组成对象可以。

函数调用

如果变量只是绑定到对象的名字,当我们把它们作为实参传递给一个函数时会发生什么?事实是,我们实际上没有传递那么多。看一下这个代码

def add_to_tree (root,value_string):

"""Given a string of characters valuestr∈g

,create or update a

series of dictionaries where the value at each level is a dictionary of

the characters that have been seen following the current character.

Example:

>>> my_string = 'abc'

>>> tree = {}

>>> add_to_tree(tree,my_string)

>>> print(tree['a']['b'])

{'c': {}}

>>> add_to_tree(tree,'abd')

>>> print(tree['a']['b'])

{'c': {},'d': {}}

>>> print(tree['a']['d'])

KeyError 'd'

"""

for character in value_string:

root = root . setdefault(character,{})

我们实际上创建了一个像 trie (译注:基数树)一样工作的自复活( auto-vivifying )字典。注意在 for 循环里我们改变了 root 参数。在这函数调用完成后, tree 仍然是同一个字典,带有某些更新。它不是这个函数调用里 root 最后的值。因此,在某种意义上, tree 正在更新;在另一种意义上,它没有。

进群“960410445”  即可获取数十套PDF以及大量的学习教程哦!

为了理解这,考虑 root 参数实际上是什么:对作为 root 参数传递的名字所援引对象的一个新绑定。在我们的例子中, root 是一开始绑定到与 tree 绑定相同的对象。它不是 tree 本身,这解释了为什么在函数里将 root 改变为新字典, tree 保持不变。你会记得,把 root 赋值为 root.setdefault(character,{}) 只是将 root 重新绑定到由 root.setdefault(character,{}) 语句创建的对象。

下面是另一个更直接明了的例子:

def list_changer (input_list):

input_list[ 0 ] = 10

input_list = range ( 1,10 )

print (input_list)

input_list[ 0 ] = 10

print (input_list)

>>> test_list = [ 5,5,5 ]

>>> list_changer(test_list)

[ 1,3,4,6,7,8,9 ]

[ 10,9 ]

>>> print test_list

[ 10,5 ]

我们第一条语句确实改变了底下列表的值(我们可以看到最后一行的输出)。不过,一旦我们通过 input_list = range(,10) 重新绑定 input_list ,我们现在引用一个完全不同的对象。我们实际上说,“将名字 input_list 绑定到这个新 list ”。在这行之后,我们没有办法再次引用原来的 input_list 参数了。

到现在为止,你应该清楚理解绑定一个名字如何工作了。还有一件事情要小心。

块与作用域

现在,名字、绑定及对象的概念应该相当熟悉了。不过,我们尚未触及的是解释器如何“找到”一个名字。为了理解我的意思,考虑下面的代码

GLOBAL_CONSTANT = 42

def print_some_weird_calculation (value):

number_of_digits = len ( str (value))

def print_formatted_calculation (result):

print ( '{value} * {constant} = {result}' . format(value = value,

constant = GLOBAL_CONSTANT,result = result))

print ( '{} {}' . format( '^' * number_of_digits,'++' ))

print ( ' Key: ^ points to your number,+ points to constant' )

print_formatted_calculation(value * GLOBAL_CONSTANT)

>>> print_some_weird_calculation( 123 )

123 * 42 = 5166

^^^ ++

Key: ^ points to your number,+ points to constant

这是一个做作的例子,但有几件事情应该会引起你的注意。首先,函数 print_formatted_calculation 如何有 value 与 number_of_digits 的访问权,尽管它们从来没有作为实参传递?其次,这两个函数如何看起来有对 GLOBAL_CONSTANT 的访问权?

答案都是与作用域( scope )相关。在 Python 中,当一个名字绑定一个对象时,这个名字仅在其作用域内可用。一个名字的作用域由创建它的块( block )确定。块就是作为单个单元执行的一个 Python 代码“块”。三个最常见的块类型是模块、类定义,以及函数体。因此,一个名字的作用域就是定义它的最里层块。

现在让我们回到最初的问题:解释器如何“找到”名字绑定到哪里(甚或它是否是一个有效名字)?它从检查最里层块的作用域开始。然后,它检查包含最里层块的作用域,然后包含这个作用域的作用域,以此类推。

函数 print_formatted_calculation 中,我们引用 value 。这首先通过检查最里层块的作用域,在这个情形里是函数体本身。当它没有找到在那里定义的 value ,它检查定义了 print_formatted_calculation 的作用域。在我们的情形里是 print_some_weird_calculation 函数体。在这里它找到了名字 value ,因此它使用这个绑定并停止查找。对 GLOBAL_CONSTANT 是一样的,它只是需要在更高一层查找:模块(或脚本)层。定义在这层的一切都被视为一个 global 名字。这些可以在任何地方访问。

一些需要注意的事情。名字的作用域扩展到任何包含在定义该名字的块内的块,除非这个名字重新绑定到这些块里的其中一个。如果 print_formatted_calculation 有行 value = 3 ,那么在 print_some_weird_calculation 中名字 value 的作用域将仅是这个函数。它的作用域将不包括 print_formatted_calculation ,因为这个块重新绑定了这个名字。

明智地使用这个能力

有两个关键字可用于告诉解释器重用一个已经存在的绑定。其他时候,每次我们绑定一个名字,它把这个名字绑定一个新对象,但仅在当前作用域中。在上面的例子里,如果我们在 print_formatted_calculation 中重新绑定 value ,它将对作为 print_formatted_calculation 围合作用域的 print_some_weird_calcuation 里的 value 没有影响。使用下面两个关键字,我们实际上可以影响我们局部作用域外的绑定。

global my_variable 告诉解释器使用在最顶层(或“ global ”作用域)中名字 my_varialbe 的绑定。在代码块里放入 global my_variable 是声明,“拷贝这个全局变量的绑定,或者如果你找不到它,在全局作用域创建这个名字 my_variable ”的一种方式。类似的, nonlocal my_variable 语句指示解释器使用在最接近的围合作用域里定义的名字 my_variable 的绑定。这是一种重新绑定一个没有定义在局部或全局作用域名字的方式。没有 nonlocal ,我们只能在本地作用域或全局作用域中修改绑定。不过,不像 global my_variable ,如果我们使用 nonlocal my_varialbe , my_variable 必须已经存在;如果找不到,它不会被创建。

为了了解实际情况,让我们编写一个快速示例:

GLOBAL_CONSTANT = 42

print (GLOBAL_CONSTANT)

def outer_scope_function ():

some_value = hex ( 0x0 )

print (some_value)

def inner_scope_function ():

nonlocal some_value

some_value = hex ( 0xDEADBEEF )

inner_scope_function()

print (some_value)

global GLOBAL_CONSTANT

GLOBAL_CONSTANT = 31337

outer_scope_function()

print (GLOBAL_CONSTANT)

# Output:

# 42

# 0x0

# 0xdeadbeef

# 31337

通过使用 global 以及 nonlocal ,我们能够使用及改变一个名字现有的绑定,而不是仅仅给这个名字赋值一个新绑定,并丢失旧的绑定。

总结

如果你看完了这篇的文章,祝贺你!希望 Python 的执行模型更加清晰了。在一篇(短得多)的后续文章中,我将通过几个例子展示如何可以有趣的方式利用一切都是对象这个事实。直到下次……

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


我最近重新拾起了计算机视觉,借助Python的opencv还有face_recognition库写了个简单的图像识别demo,额外定制了一些内容,原本想打包成exe然后发给朋友,不过在这当中遇到了许多小问题,都解决了,记录一下踩过的坑。 1、Pyinstaller打包过程当中出现warning,跟d
说到Pooling,相信学习过CNN的朋友们都不会感到陌生。Pooling在中文当中的意思是“池化”,在神经网络当中非常常见,通常用的比较多的一种是Max Pooling,具体操作如下图: 结合图像理解,相信你也会大概明白其中的本意。不过Pooling并不是只可以选取2x2的窗口大小,即便是3x3,
记得大一学Python的时候,有一个题目是判断一个数是否是复数。当时觉得比较复杂不好写,就琢磨了一个偷懒的好办法,用异常处理的手段便可以大大程度帮助你简短代码(偷懒)。以下是判断整数和复数的两段小代码: 相信看到这里,你也有所顿悟,能拓展出更多有意思的方法~
文章目录 3 直方图Histogramplot1. 基本直方图的绘制 Basic histogram2. 数据分布与密度信息显示 Control rug and density on seaborn histogram3. 带箱形图的直方图 Histogram with a boxplot on t
文章目录 5 小提琴图Violinplot1. 基础小提琴图绘制 Basic violinplot2. 小提琴图样式自定义 Custom seaborn violinplot3. 小提琴图颜色自定义 Control color of seaborn violinplot4. 分组小提琴图 Group
文章目录 4 核密度图Densityplot1. 基础核密度图绘制 Basic density plot2. 核密度图的区间控制 Control bandwidth of density plot3. 多个变量的核密度图绘制 Density plot of several variables4. 边
首先 import tensorflow as tf tf.argmax(tenso,n)函数会返回tensor中参数指定的维度中的最大值的索引或者向量。当tensor为矩阵返回向量,tensor为向量返回索引号。其中n表示具体参数的维度。 以实际例子为说明: import tensorflow a
seaborn学习笔记章节 seaborn是一个基于matplotlib的Python数据可视化库。seaborn是matplotlib的高级封装,可以绘制有吸引力且信息丰富的统计图形。相对于matplotlib,seaborn语法更简洁,两者关系类似于numpy和pandas之间的关系,seabo
Python ConfigParser教程显示了如何使用ConfigParser在Python中使用配置文件。 文章目录 1 介绍1.1 Python ConfigParser读取文件1.2 Python ConfigParser中的节1.3 Python ConfigParser从字符串中读取数据
1. 处理Excel 电子表格笔记(第12章)(代码下载) 本文主要介绍openpyxl 的2.5.12版处理excel电子表格,原书是2.1.4 版,OpenPyXL 团队会经常发布新版本。不过不用担心,新版本应该在相当长的时间内向后兼容。如果你有新版本,想看看它提供了什么新功能,可以查看Open
1. 发送电子邮件和短信笔记(第16章)(代码下载) 1.1 发送电子邮件 简单邮件传输协议(SMTP)是用于发送电子邮件的协议。SMTP 规定电子邮件应该如何格式化、加密、在邮件服务器之间传递,以及在你点击发送后,计算机要处理的所有其他细节。。但是,你并不需要知道这些技术细节,因为Python 的
文章目录 12 绘图实例(4) Drawing example(4)1. Scatterplot with varying point sizes and hues(relplot)2. Scatterplot with categorical variables(swarmplot)3. Scat
文章目录 10 绘图实例(2) Drawing example(2)1. Grouped violinplots with split violins(violinplot)2. Annotated heatmaps(heatmap)3. Hexbin plot with marginal dist
文章目录 9 绘图实例(1) Drawing example(1)1. Anscombe’s quartet(lmplot)2. Color palette choices(barplot)3. Different cubehelix palettes(kdeplot)4. Distribution
Python装饰器教程展示了如何在Python中使用装饰器基本功能。 文章目录 1 使用教程1.1 Python装饰器简单示例1.2 带@符号的Python装饰器1.3 用参数修饰函数1.4 Python装饰器修改数据1.5 Python多层装饰器1.6 Python装饰器计时示例 2 参考 1 使
1. 用GUI 自动化控制键盘和鼠标第18章 (代码下载) pyautogui模块可以向Windows、OS X 和Linux 发送虚拟按键和鼠标点击。根据使用的操作系统,在安装pyautogui之前,可能需要安装一些其他模块。 Windows: 不需要安装其他模块。OS X: sudo pip3
文章目录 生成文件目录结构多图合并找出文件夹中相似图像 生成文件目录结构 生成文件夹或文件的目录结构,并保存结果。可选是否滤除目录,特定文件以及可以设定最大查找文件结构深度。效果如下: root:[z:/] |--a.py |--image | |--cat1.jpg | |--cat2.jpg |
文章目录 VENN DIAGRAM(维恩图)1. 具有2个分组的基本的维恩图 Venn diagram with 2 groups2. 具有3个组的基本维恩图 Venn diagram with 3 groups3. 自定义维恩图 Custom Venn diagram4. 精致的维恩图 Elabo
mxnet60分钟入门Gluon教程代码下载,适合做过深度学习的人使用。入门教程地址: https://beta.mxnet.io/guide/getting-started/crash-course/index.html mxnet安装方法:pip install mxnet 1 在mxnet中使
文章目录 1 安装2 快速入门2.1 基本用法2.2 输出图像格式2.3 图像style设置2.4 属性2.5 子图和聚类 3 实例4 如何进一步使用python graphviz Graphviz是一款能够自动排版的流程图绘图软件。python graphviz则是graphviz的python实