加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 编程开发 > Python > 正文

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

发布时间:2020-12-17 00:31:26 所属栏目:Python 来源:网络整理
导读:Python 新手通常对他们自己的代码感到惊讶。他们期望 A ,但看起来没有原因,发生了 B 。许多这些“惊奇”的根本原因是混淆了 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 的执行模型更加清晰了。在一篇(短得多)的后续文章中,我将通过几个例子展示如何可以有趣的方式利用一切都是对象这个事实。直到下次……

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读