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

作者:Jeff Knupp

原文地址:https://jeffknupp.com/blog/2013/02/14/drastically-improve-your-python-understanding-pythons-execution-model/

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

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

一切都是一个对象?

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

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

Python解释器通常搞混初学者的一个特性是,在一个赋值给一个用户定义对象的“变量”上调用print()时会发生什么(稍后我会解释引号)。使用内置类型,通常打印出一个正确的值,像在stringint上调用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 "<stdin>", 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里,任何你可以放在等号右手侧的东西是(或创建了)一个对象。10Hello 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, 02, 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类的一个对象)。我们可以随心重新绑定它。在上面的例子中,我们将datetimedatetime属性绑定到我们的新类,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 "<stdin>", line 1, 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 `value_string`, 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正在更新;在另一种意义上,它没有。

为了理解这,考虑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, 2, 3, 4, 5, 6, 7, 8, 9]

[10, 2, 3, 4, 5, 6, 7, 8, 9]

>>> print test_list

[10, 5, 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('\nKey: ^ 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如何有valuenumber_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_varialbemy_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的执行模型更加清晰了。在一篇(短得多)的后续文章中,我将通过几个例子展示如何可以有趣的方式利用一切都是对象这个事实。直到下次……

If you found this post useful, check out Writing Idiomatic Python. It's filled with common Python idioms and code samples showing the right and wrong way to use them.(广告,不翻了)

 

猜你喜欢

转载自blog.csdn.net/wuhui_gdnt/article/details/84645854
今日推荐