隨手扎
Python的資料模型(Data Model)的特殊成員(Special Member)
上一篇寫到,覆寫Python操作子(Override Operator)。其中提到operator這個Package,並對類別實現特殊方法,來覆寫運算子的行為。不過這麼寫不完全正確,只是Operator Package的頁面清楚的多,而這覆寫的動作,實際更直接與Python資料模型中的特殊成員(特殊屬性&特殊方法)(Data Model - Special Member(Special Attribute & Special Method))相關。
Python的資料模型
曾經看過:
「程式設計」就是「資料結構」+「演算法」
這篇不算寫下,那些大多人都已經了解的list
、dict
、set
、tuple
等等。我重點還是會放在特殊成員上。不過在整理、理解消化文件的同時,看到一些比較少用的概念和內置類型。
Python一切皆為物件(Object)
當你在Python指定值給一個變數時,幾乎總是新建一個物件。這個物件在型態上是不可變的(與資料上不可變有差異),比較接近EMCAScript的const
。每一個物件有一個唯一id,可以用id()
獲取,在CPython就是物件存在記憶體的位置。物件可以被多次引用,並在不存在引用時,由Python決定何時做記憶體回收。
※ 從C/C++的角度來說,Python的所有變數都是指標(Point)。只是Python沒有指標運算。
var1 = []
var2 = var1
id(var1) # => 140572698774216
id(var2) # => 140572698774216
id(var1) == id(var2) # => True
var1 is var2 # => True
################
var2 = [] # create a new object
id(var2) # => 140572698794504
var1 is var2 # => False, not same object
var1 == var2 # ==> True, value is equal
原子型態 & 複合型態
OK, 已經知道變數本身是不可變得,總是在指定後更新所指定的物件。那這個所指定的物件的?基本上分兩種型態,「原子型態」和「複合型態」。原子型態不可並,而複合型態由原子型態或複合型態組成,可能可以替換所組成的成員。
a = [1,2,3] # 複合型態,由3個原子整數型態組成。
a[2] = -3 # => a == [1,2,-3]。 變換複合型態成員,但a所指向物件不變。
a.append(4) # 添加新成員,但a所指向物件不變。
a = [] # 指定a一個新物件。原物件可能被記憶體回收。
※ 這只是一種概念上的思維。從不同角度上可能有不同結果。
可變 vs. 不可變
原子型態全都是不可變得,所以可以安全的被多次引用。(下面指定b值,可能不直接建立新物件)
a = 1
b = 1
a is b # => True
id(a) # => 10914496
id(b) # => 10914496
在Python,String也總是不可變得。每次String的計算,總是產生新物件。
a = "hello"
b = "hello"
a is b # => True
id(a) # => 140572792589480
id(b) # => 140572792589480
# a[0] = "y" # TypeError: 'str' object does not support item assignment
a[0] # => 'h', but a[0] is accessable
id(a[0]) # => 140572793261336, point to other object
a += ", world" # create new object
a is b # => False
id(a) # => 140572792580912
有趣的是,Tuple跟List很像,但是是不可變得,卻又跟String有差異:
a = (1,2,3,4,5)
b = (1,2,3,4,5)
a is b # => False
id(a) # => 140174493238464
id(b) # => 140174493238552
# a[0] = "y" # TypeError: 'tuple' object does not support item assignment
a[0] # => 1, but a[0] is accessable
id(a[0]) # => 10914496, point to other object
a += (6,7,8,9) # create new object
id(a) # => 140174399490232
※ 從上面行為來看,Python的String是原子型別,但Tuple是複合型別(Python也沒有char型態)。
總的來說,是由Python建立、消滅物件,分配、回收記憶體。就算一個無法被修改的型態,並不能保證一個直接指定一個,會引用到相同物件。要確保兩變數指向相同物件的最好辦法還是a = b
。
在CPython的實現裡,可能保證了[-5, 256]
會有唯一值。CPython的浮點數印象中是雙精度浮點數,對於單精度浮點數的支持,被認為效益不大,所佔記憶體空間較多。所以浮點數同值的變數,在CPython也可能指向不同物件。上例短字串雖然看起來好像和小整數有相同結果,但是在過長的字串或是計算後的字串,儘管字面值可能相同,但可能就是不同物件。
a = "a"
b = "ab"
a = a + "b"
a is b # => False
a == b # => True
奇特的內置類型
None
NotImplemented
Ellipsis
numbers.Number
numbers.Integral
numbers.Real
numbers.Complex
這些可能除了None
外,其他都比較少用?
此外,這些定義在未來有可能有改變。
不過numbers.Number
,還可以這樣用:
特殊成員(Special Members)
Python有許多內建函數,這些內建函數還對應一些特殊成員(不完全)。譬如:
常見的特殊成員
built-in function | special member |
---|---|
help() | __doc__ |
dir() | __dir__() |
getattr() | __getattr()__ |
setattr() | __setattr()__ |
str() | __str()__ |
repr() | __repr__() |
copy.copy() | __copy__() |
基本上_
的在Python有特殊意義,尤其是__keyword__
的形式更是。有篇我覺得寫得不錯,推薦下:在 python 中,常常看到 _ 以及 __ 到底是什麼。
特殊屬性(Special Attributes)
特殊方法(Special Methods)
__new__()
、__init__()
、__del__()
、__call()__
一個物件建立時,會嘗試呼叫類別的__call()__
。通常,__call__()
會在執行__new()__
建立實體物件,在加以__init()__
初始化物件。大多時候,定義一個新類別只需要決定如何初始化物件,不用考慮如何建立物件。
※ 但是可以設計Meta Class,來改變行為。
※ del obj
不直接調用obj.__del()__
,前者只將引用計數器歸減一,後者在計數器歸零時,才可能被調用。
__call()__
同時也決定物件實體是否可以呼叫。(類別也是type
的實體)
isinstance(int, type) # => True
type(type) # => <class 'type'>
type(object) # => <class 'type'>
isinstance(type,object) # => True
isinstance(object, type) # => True
isinstance(object(), type) # => False
※ type類別是自己本身,type和type實體繼承object;object類別是type,object繼承type
__str__()
、__repr__()
兩者對應str()
和repr
。有助於Print Debug。
__getattr()__
、__setattr()__
、__delattr()__
對應於getattr()
、setattr()
、del obj.attr
。
描述器(descriptor) - __get()__
、__set()__
、__delete()__
、__set_name__
這與屬性取值與賦予值有關,也可以參考property
,其可以便利實現描述器的getter
、setter
和deleter
。
※ 有點像Java裡的Getter、Setter或是Kotlin的資料類別(Data Class)
__dict__
、__weakref__
、__slots__
物件的屬性,或說是內部的命名空間(namespace)。
__dir()__
對應於dir()
。我通常用於查找物件可用的屬性和方法。
__len__()
對應於len()
。
__getitem__()
、__setitem__()
、__delitem__()
與下標運算(subscript operator, obj[item]
)有關。
class myList(list):
def __getitem__(self, key):
if type(key) is int:
return list(self)[key]
elif type(key) is slice:
return list(self)[key]
elif type(key) is tuple:
if len(key) == 1:
return list(self)[key[0]]
elif len(key) == 2:
return list(self)[key[0]:key[1]]
else:
return list(self)[key[0]:key[1]:key[2]]
a = myList([1,2,3,4,5,6,7])
a[0] # => 1
a[:] # => [1,2,3,4,5,6,7]
a[0,] # => 1
a[0,-1] # => [1,2,3,4,5,6]
上面myList
實現並不是非常好,但是體現了pandas.DataFrame.loc
的行為。1,
會得到一個tuple(1,)
,在[]
裡的1:
會得到傳入一個slice(1, None, None)
,所以obj[1:-1,1]
和obj[slice(1,-1,1)]
是等價的。
其他
__init_subclass__()
__instancecheck__()
__subclasscheck__()
__class_getitem__()