Python 新手通常对他们自己的代码感到惊讶。他们期望 A ,但看起来没有原因,发生了 B 。许多这些“惊奇”的根本原因是混淆了 Python 执行模型。它这样的,如果向你解释一次,一些 Python 概念在变得清晰之前,看起来是模糊不清的。仅仅靠你自己去 “ 弄清楚 ” 也是很困难的,因为它要求对核心语言概念,如变量、对象及函数的思考有根本性的转变。
在本文,我将帮助你理解,在创建变量或调用函数等常见操作背后发生看什么。因此,你将编写更清晰、更易于理解的代码。你还成为一个更好(更快)的代码读者。所需要的就是忘记你所知道的关于编程的一切……
一切都是一个对象?
在大多数人第一次听到在 Python 里“一切都是一个对象”时,这触发了对 Java 等语言的记忆重现,其中用户编写的每一件东西都封装在一个对象里。其他人假设这意味着在 Python 解释器的实现中,一切都实现为对象。第一个解释的错误的;第二个成立,但不是特别有趣(对我们的目的)。这个短语实际指的是所有“事物”这个事实,不管它们是值、类、函数、对象实例(显然)以及几乎其他语言构造,概念上是一个对象。
一切都是对象意味着什么?它意味着提到的“事物”有我们通常与对象联系起来的所有属性(以面向对象的观念);类型有成员函数,函数有属性( attribute ),模块可以作为实参传递等。它对 Python 中的赋值如何工作具有重要的意义
Python 解释器通常搞混初学者的一个特性是,在一个赋值给一个用户定义对象的“变量”上调用 print() 时会发生什么(稍后我会解释引号)。使用内置类型,通常打印出一个正确的值,像在 string 及 int 上调用 print() 时。但于简单的用户定义类,解释器吐出一些难看的字符串,像:
>>>classFoo():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 "
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 里一切本质上是绑定到对象的名字,我们可以做像这样(有趣)的蠢事:
>>>importdatetime
>>>importimp
>>> datetime . datetime . now()
datetime . datetime( 2013,02,14,53,59,608842 )
>>>classPartyTime():
...def__call__ ( self,* args):
... imp . reload(datetime)
... value = datetime . datetime( * args)
... datetime . datetime = self
...returnvalue
...
...def__getattr__ ( self,value):
...ifvalue == 'now' :
...returnlambda:print( 'Party Time!' )
...else:
... imp . reload(datetime)
... value = getattr (datetime . datetime,value)
... datetime . datetime = self
...returnvalue
>>> 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 的“值”概念上只是一系列到对象绑定不可变的名字。要注意的关键是绑定是不可变的,不是它们绑定的对象。
这意味着下面是完全合法的:
>>>classFoo():
...def__init__ ( self ):
... self . value = 0
...def__str__ ( self ):
...returnstr ( self . value)
...def__repr__ ( self ):
...returnstr ( 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 "
TypeError : 'tuple' object doesnotsupport item assignment
>>> f . value = 999
>>>print(f)
999
>>>print(foo_tuple)
( 999,999 )