寫在前面
如非特別說明,下文均基于
Python3
1、一切皆對象
Python
哲學(xué):
Python中一切皆對象
1.1 數(shù)據(jù)模型-對象,值以及類型
對象是Python
對數(shù)據(jù)的抽象。Python
程序中所有的數(shù)據(jù)都是對象或?qū)ο笾g的關(guān)系表示的。(在某種意義上,為順應(yīng)馮·諾依曼“存儲式計算機”的模型,Python
中的代碼也是對象。)
Python
中每一個對象都有一個身份標識,一個值以及一個類型。對象創(chuàng)建后,其身份標識絕對不會改變;可以把身份標識當(dāng)做對象在內(nèi)存中的地址。is
操作符比較兩個對象的身份標識;id()
函數(shù)返回表示對象身份標識的整數(shù)。
CPython實現(xiàn)細節(jié): 在CPython
解釋器的實現(xiàn)中,id(x)
函數(shù)返回存儲x
的內(nèi)存地址
對象的類型決定了對象支持的操作(例如,對象有長度么?),同時也決定了該類型對象可能的值。type()
函數(shù)返回對象的類型(這個類型本身也是一個對象)。與其身份標識一樣,對象的類型也是不可改變的[1]。
一些對象的值可以改變。可改變值的對象也稱作可變的(mutable);一旦創(chuàng)建,值恒定的對象也叫做 不可變的(immutable)。(當(dāng)不可變?nèi)萜鲗ο笾邪瑢勺儗ο蟮囊脮r,可變對象值改變時,這個不可變?nèi)萜鲗ο笾狄脖桓淖兞?;然而,不可變?nèi)萜鲗ο笕员徽J為是不可變的,因為對象包含的值集合確實是不可改變的。因此,不可變性不是嚴格等同于擁有不可變的值,它很微妙。) (譯注:首先不可變?nèi)萜鲗ο蟮闹凳且粋€集合,集合中包含了對其他對象的引用;那么這些引用可以看做地址,即使地址指向的內(nèi)容改變了,集合中的地址本身是沒有改變的。所以不可變?nèi)萜鲗ο筮€是不可變對象。) 對象的可變性取決于其類型;例如,數(shù)字,字符串和元組是不可變的,但字典和列表是可變的。
對象從不顯式銷毀;當(dāng)對象不可達時會被垃圾回收(譯注:對象沒有引用了)。一種解釋器實現(xiàn)允許垃圾回收延時或者直接忽略——這取決于垃圾回收是如何實現(xiàn)的,只要沒有可達對象被回收。
CPython實現(xiàn)細節(jié): CPython
解釋器實現(xiàn)使用引用計數(shù)模式延時探測循環(huán)鏈接垃圾,這種方式可回收大多數(shù)不可達對象,但并不能保證循環(huán)引用的垃圾被回收。查看gc
模塊的文檔了解控制循環(huán)垃圾回收的更多信息。其他解釋器實現(xiàn)與CPython
不同,CPython
實現(xiàn)將來也許會改變。因此不能依賴垃圾回收器來回收不可達對象(因此應(yīng)該總是顯式關(guān)閉文件對象。)。
需要注意,使用工具的調(diào)試跟蹤功能可能會導(dǎo)致應(yīng)該被回收的對象一直存活,使用try
...
except
語句捕獲異常也可以保持對象的存活。
一些對象引用了如文件或者窗口的外部資源。不言而喻持有資源的對象被垃圾回收后,資源也會被釋放,但因為沒有機制保證垃圾回收一定會發(fā)生,這些資源持有對象也提供了顯式釋放外部資源的方式,通常使用close()
方法。強烈推薦在程序中顯式釋放資源。try
...
finally
語句和with
語句為釋放資源提供了便利。
一些對象包含對其他對象的引用,這些對象被稱作 容器。元組,列表和字典都是容器。引用的容器值的一部分。大多數(shù)情況下,談?wù)撊萜鞯闹禃r,我們暗指容器包含的對象值集合,而不是對象的身份標識集合;然而,談?wù)撊萜鞯目勺冃詴r,我們暗指容器包含的對象的身份標識。因此,如果不可變對象(如元組)包含對可變對象的引用,可變對象改變時,其值也改變了。
類型影響對象的絕大多數(shù)行為。在某些情況下甚至對象的身份標識的重要性也受到影響:對于不可變類型,計算新值的操作實際上可能會返回已存在的,值和類型一樣的對象的引用,然而對于可變對象來說這是不可能的。例如,語句a = 1; b = 1
執(zhí)行之后,a
和b
可能會也可能不會引用具有相同值得同一個對象,這取決于解釋器實現(xiàn)。但是語句c = []; d = []
執(zhí)行之后,可以保證c
和d
會指向不同的,唯一的新創(chuàng)建的空列表。(注意 c = d = []
分配相同的對象給c
和d
)
Note: 以上翻譯自 《The Python Language References#Data model# Objects, values, types》 3.6.1版本。
1.2 對象小結(jié)
官方文檔已經(jīng)對Python
對象做了詳細的描述,這里總結(jié)一下。
對象的三個特性:
-
身份標識
唯一標識對象;不可變;CPython
解釋器實現(xiàn)為對象的內(nèi)存地址。
操作:id()
,內(nèi)建函數(shù)id()
函數(shù)返回標識對象的一個整數(shù);is
比較兩個對象的身份標識。
示例:>>> id(1) 1470514832 >>> 1 is 1 True
-
類型
決定對象支持的操作,可能的值;不可變。
操作:type()
,內(nèi)建函數(shù)返回對象的類型
示例:>>> type('a') <class 'str'>
-
值
數(shù)據(jù),可變/不可變
操作:==
操作符用于比較兩個對象的值是否相等,其他比較運算符比較對象間大小情況。
示例:>>> 'python' 'python' >>> 1 == 2 False
可變與不可變:一般認為,值不可變的對象是不可變對象,值可變的對象是可變對象,但是要注意不可變集合對象包含可變對象引用成員的情況。
Python
中的對象:
# -*- coding: utf-8 -*-
# filename: hello.py
'a test module'
__author__ = 'Richard Cheng'
import sys
class Person(object):
''' Person class'''
def __init__(self, name, age):
self.name = name
self.age = age
def tset():
print(sys.path)
p = Person('Richard', 20)
print(p.name, ':', p.age)
def main():
tset()
if __name__ == '__main__':
main()
這段Python
代碼中有很多對象,包括hello
這個模塊對象,創(chuàng)建的Person
類對象,幾個函數(shù)如test
, main
函數(shù)對象,數(shù)字,字符串,甚至代碼本身也是對象。
2、名字即“變量”
幾乎所有語言中都有“變量”的說法,嚴格說來,Python
中的變量不應(yīng)該叫變量,稱為名字更加貼切。
以下翻譯自 Code Like a Pythonista: Idiomatic Python # Python has "names"
2.1 其他語言有變量
其他語言中,為變量分配值就像將值放到“盒子”里。
int a = 1;
盒子a
現(xiàn)在有了一個整數(shù)1
。
為同一個變量分配值替換掉盒子的內(nèi)容:
a =2;
現(xiàn)在盒子
a
中放了整數(shù)2
將一個變量分配給另一個變量,拷貝變量的值,并把它放到新的盒子里:
int b = a;
b
是第二個盒子,裝有整數(shù)2的拷貝。盒子a
有一份單獨的拷貝。
2.2 Python有名字
Python
中,名字或者標識符就像將一個標簽捆綁到對象上一樣。
a = 1
這里,整數(shù)對象1
有一個叫做a
的標簽。
如果重新給a
分配值,只是簡單的將標簽移動到另一個對象:
a = 2
現(xiàn)在名字
a
貼到了整數(shù)對象2
上面。原來的整數(shù)對象1不再擁有標簽a
,或許它還存在,但是不能通過標簽a
訪問它了(當(dāng)對象沒有任何引用時,會被回收。)
如果將一個名字分配給另一名字,只是將另一個名字標簽捆綁到存在的對象上:
b = a
名字b
只是綁定到與a
引用的相同對象上的第二個標簽而已。
雖然在Python
中普遍使用“變量”(因為“變量”是普遍術(shù)語),真正的意思是名字或者標識符。Python
中的變量是值得標簽,不是裝值得盒子。
2.3 指針?引用?名字?
C/C++
中有指針,Java
中有引用,Python
中的名字在一定程度上等同于指針和引用。
2.1節(jié)中其他語言的例子,也只是針對于它們的基本類型而言的,若是指針或者引用,表現(xiàn)也跟Python
的名字一樣。這也在一定程度上說明了Python
將面向?qū)ο筘瀼氐酶訌氐住?/p>
2.4 名字支持的操作
可以對一個變量做什么?聲明變量,使用變量,修改變量的值。名字作為Python
中的一個重要概念,可以對它做的操作有:
- 定義;名字需要先定義才能使用,與變量需要先聲明一樣。
- 綁定:名字的單獨存在沒有意義,必須將它綁定到一個對象上。
- 重綁定:名字可以重新引用另一個對象,這個操作就是重綁定。
- 引用:為什么要定義名字,目的是使用它。
3、綁定的藝術(shù)
名字以及對象,它們之間必然會發(fā)生些什么。
3.1 變量的聲明
其他如C/C++
和Java
的高級語言,變量在使用前需要聲明,或者說定義。以下在Java
中聲明變量:
public static void main(String[] args) {
int i = 0; // 先聲明,后使用
System.out.println(i); // 使用變量i
}
這樣,在可以訪問到變量i
所在作用域的地方,既可以使用i
了。還有其他聲明變量的方法么?好像沒有了。
3.2 名字的定義
Python
中有多種定義名字的途徑,如函數(shù)定義,函數(shù)名就是引用函數(shù)對象的名字;類定義,類名就是指向類對象的名字,模塊定義,模塊名就是引用模塊對象的名字;當(dāng)然,最直觀的還是賦值語句。
賦值語句
官方對賦值語句做了這樣的說明(地址):
Assignment statements are used to (re)bind names to values and to modify attributes or items of mutable objects.
即:
賦值語句被用來將名字綁定或者重綁定給值,也用來修改可變對象的屬性或項
那么,我們關(guān)心的,就是賦值語句將名字和值(對象)綁定起來了。
看一個簡單的賦值語句:
a = 9
Python
在處理這條語句時:
- 首先在內(nèi)存中創(chuàng)建一個對象,表示整數(shù)
9
:
- 然后創(chuàng)建名字
a
,并把它指向上述對象:
上述過程就是通過賦值語句的名字對象綁定了。名字首次和對象綁定后,這個名字就定義在當(dāng)前命名空間了,以后,在能訪問到這個命名空間的作用域中可以引用該名字了。
3.3 引用不可變對象
定義完名字之后,就可以使用名字了,名字的使用稱為“引用名字”。當(dāng)名字指向可變對象和不可變對象時,使用名字會有不同的表現(xiàn)。
a = 9 #1
a = a + 1 #2
語句1執(zhí)行完后,名字a
指向表示整數(shù)9
的對象:
由于整數(shù)是不可變對象,所以在語句2處引用名字a
,試圖將表示整數(shù)9
的對象 + 1
,但該對象的值是無法改變的。因此就將該對象表示的整數(shù)值9
加1
,以整數(shù)10
新建一個整數(shù)對象:
接下來,將名字a
重綁定
到新建對象上,并移除名字對原對象的引用:
使用id()
函數(shù),可以看到名字a
指向的對象地址確實發(fā)生了改變:
>>> a = 9
>>> id(a)
1470514960
>>> a = a + 1
>>> id(a)
1470514976
3.4 引用可變對象
3.4.1 示例1:改變可變對象的值
可變對象可以改變其值,并且不會造成地址的改變:
>>> list1 = [1]
>>> id(list1)
42695136
>>> list1.append(2)
>>> id(list1)
42695136
>>> list1
[1, 2]
>>>
執(zhí)行語句list1 = [1]
,創(chuàng)建一個list
對象,并且其值集中添加1
,將名字list1
指向該對象:
執(zhí)行語句list1.append(2)
,由于list
是可變對象,可以直接在其值集中添加2
:
值得改變并沒有造成list1
引用的對象地址的改變。
3.4.2 示例2:可變對象循環(huán)引用
再來看一個比較“奇怪”的例子:
values = [1, 2, 3]
values[1] = values
print(values)
一眼望去,期待的結(jié)果應(yīng)該是
[1, [1, 2, 3], 3]
但實際上結(jié)果是:
[1, [...], 3]
我們知道list
中的元素可以是各種類型的,list
類型是可以的:
3.4.3 示例3:重綁定可變對象
觀察以下代碼段:
>>> list1 = [1]
>>> id(list1)
42695136
>>> list1 = [1, 2]
>>> id(list1)
42717432
兩次輸出的名字list1
引用對象的地址不一樣,這是因為第二次語句list 1 = [1, 2]
對名字做了重綁定:
3.5 共享對象
當(dāng)兩個或兩個以上的名字引用同一個對象時,我們稱這些名字共享對象。共享的對象可變性不同時,表現(xiàn)會出現(xiàn)差異。
3.5.1 共享不可變對象
函數(shù)attempt_change_immutable
將參數(shù)i
的值修改為2
def attempt_change_immutable(i):
i = 2
i = 1
print(i)
attempt_change_immutable(i)
print(i)
Output:
1
1
如果你對輸出不感到意外,說明不是新手了 _。
- 首先,函數(shù)的參數(shù)
i
與全局名字i
不是在同一命名空間中,所以它們之間不相互影響。 - 調(diào)用函數(shù)時,將兩個名字
i
都指向了同一個整數(shù)對象。 - 函數(shù)中修改
i
的值為2
, 因為整數(shù)對象不可變,所以新建值為2
的整數(shù)對象,并把函數(shù)中的名字i
綁定到對象上。 - 全局名字
i
的綁定關(guān)系并沒有被改變。
值得注意的是,這部分內(nèi)容與命名空間和作用域有關(guān)系,另外有文章介紹它們,可以參考。
3.5.2 共享可變對象
函數(shù)attempt_change_mutable
為列表增加字符串。
def attempt_change_mutable(list_param):
list_param.append('test')
list1 = [1]
print(list1)
attempt_change_mutable(list1)
print(list1)
output:
[1]
[1, 'test']
可以看到函數(shù)成功改變了列表list1
的值。傳遞參數(shù)時,名字list_param
引用了與名字list1
相同的對象,這個對象是可變的,在函數(shù)中成功修改了對象的值。
首先,名字list_param
與名字list1
指向?qū)ο螅?/p>
然后,通過名字list_param
修改了對象的值:
最后,這個修改對名字list1
可見。
3.6 綁定何時發(fā)生
總的來說,觸發(fā)名字對象綁定的行為有以下一些:
賦值操作;
a = 1
-
函數(shù)定義;
def test(): pass
將名字test
綁定到函數(shù)對象
-
類定義:
class Test(object): pass
將名字Test
綁定到類對象
-
函數(shù)傳參;
def test(i): pass test(1)
將名字i
綁定到整數(shù)對象1
-
import
語句:import sys
將名字sys
綁定到指定模塊對象。
-
for
循環(huán)for i in range(10): pass
每次循環(huán)都會綁定/重綁定名字i
-
as
操作符with open('dir', 'r') as f: pass try: pass except NameError as ne: pass
with open
語句,異常捕獲語句中的as
都會發(fā)生名字的綁定
4、其他說明
待續(xù)。。。
參考
- The Python Language References#Data model# Objects, values, types
- Python的名字綁定
- Python一切皆對象
- Code Like a Pythonista: Idiomatic Python
- python基礎(chǔ)(5):深入理解 python 中的賦值、引用、拷貝、作用域
腳注
<span id="jump">[1]</span> 在特定的控制條件下,改變對象的類型是可能的。但不是一種明智的做法,如果處理不當(dāng)?shù)脑?,會發(fā)生一些奇怪的行為。