Python 入門 —— 面向對象編程
面向對象編程是一種編程范式,通過將對象作為程序的基本單元,每個對象之間可以相互傳遞信息,并通過各自的方法對信息進行處理,從而達到程序處理的目的。
而面向過程編程則是將程序視為一系列順序執行命令的集合,通過將程序分割為一個個功能函數,降低程序的復雜度。簡單來說,面向對象更像是團隊合作,組內每個成員具有明確任務和職責;而面向過程更像是排隊,一個接一個,后一個接前一個。
面向對象,首先需要明確兩個概念:類和對象。類是對客觀世界的抽象,是對具有相同特性和行為的對象進行抽象剝離;而對象是類的實例,是客觀存在的事物。
舉例來說,人類與人即是類與對象的關系,人類并不是客觀存在的事物,它是對所有人的統稱,人類的包含一些固有屬性包括:性別、年齡、膚色等,人類也存在一些行為:吃、看、想等。而每個人都是一個客觀存在的,雖然都有性別、年齡但各不相同(男女老少),每個人的行為方式基本一樣,但也存在差異(中國人說中國話、英國人說英語)。
回想一下我們前面所介紹的內置數據結構,其中就包含了類的概念:數據結構+算法,數據結構定義了類的固有屬性及組織方式,使用算法來定義類的行為方式。注意類與數據結構的區別,兩者并不是一樣的。
面向對象的三個特性:
- 封裝:隱藏不需要與外部交互的代碼的實現細節,僅保留部分接口。眼睛將我們看到的事物景象傳遞到大腦,這是一個非常復雜的轉換過程,交給專業的人去探尋就行,我們要做的只是用眼睛去發現和感受美。
- 繼承:顧名思義,一個類從另一個類中繼承其屬性和方法,并可以重寫或添加某些屬性和方法。基于人類,我們又可以分出黃種人、黑人和白人,各自擁有不同的膚色,但是都是人類,具有人類的特征和行為方式。
- 多態:由繼承所產生的不同類能夠對同一消息做出不同反應。同樣是說話,中國人說的是漢語、英國人說的是英語。
下面我們將從 Python
的角度來分別來介紹兩種不同的面向對象編程。Python
本身便是一門面向對象編程語言,在設計之初便加入了面向對象功能,在 Python
中,一切皆為對象。
類與實例
定義類
在 Python
中,使用 class
關鍵字來定義類,我們定義一個不具有任何功能 Person
類
class Person:pass
其中, pass
是空語句,作為一個占位符,不執行任何操作,同時保持程序結構的完整性,可以在后續為其添加功能。
創建一個 Person
類的實例對象 p
p = Person()
類的屬性和方法都是使用 .
運算符來訪問,相當于訪問實例對象命名空間內的變量。
類和實例對象的命名空間以動態數據結構(字典)的方式實現,可以使用內置屬性 __dict__
來訪問。
p.__dict__
# {}
vars
函數可以訪問任何對象的 __dict__
屬性值,如果對象沒有該屬性,則會引發異常
vars(p)
# {}
或者使用 dir
函數來訪問,相當于 object.__dict__.keys()
,但是會對列表進行排序并添加一些系統屬性
dir(p)
# [
# '__class__', '__delattr__', '__dict__', '__dir__',
# '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
# '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__',
# '__lt__', '__module__', '__ne__', '__new__', '__reduce__',
# '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
# '__subclasshook__', '__weakref__'
# ]
實例屬性和方法
Python 是一門動態類型語言,我們可以在創建一個類的實例之后,為其添加屬性和方法,例如
# 實例添加屬性
p.name = 'Tom'
# 為類添加方法
def say_hello(self):print('Hello', self.name)
Person.say_hello = say_hellop.__dict__
# {'name': 'Tom'}
但是我們通常不會這么做,一般都是在類的內部進行定義,并使用專門的構造函數來定義實例的創建
class Person:def __init__(self, name, age):self.name = nameself.age = agedef say_hello(self):print('Hello', self.name)
其中 __init__
是 Python
眾多魔法方法中的一個,是類的初始化函數,定義如何初始化一個類實例對象。例如,上述初始化函數表示在創建一個對象時,需要傳入兩個參數,第一個參數賦值給實例的 name
屬性,第二個參數賦值給實例的 age
屬性。
實例方法的第一個參數 self
代表的是實例本身,即每創建一個新的實例,self
都會指向這一實例而不是類,每個實例存儲在不同的內存地址中。因此,self.name
訪問的是其指向的實例的 name
屬性,而不是其他實例或類本身的屬性。
記住:所有實例屬性和實例方法的第一個參數都表示實例本身,且所有實例方法只能通過實例對象來調用。例如,我們創建兩個實例對象
tom = Person(name='Tom', age=19)
jay = Person(name='jay', age=20)
tom.say_hello()
# Hello Tom
jay.say_hello()
# Hello jay
不同的實例對象的 say_hello
,引用了其本身的屬性值,而不是其他實例對象的值。
其實,實例方法的第一個參數名稱可以是任意的(重要的是位置,而不是名字),如 myself
也是可以的,但是最好都統一寫出 self
。
class Person:def __init__(myself, name, age):myself.name = namemyself.age = age
類屬性和方法
不同于實例方法和屬性,類屬性和方法是在所有實例中共享的,提供類的默認行為。類屬性可以使用類名類訪問,例如
class Person:counter = 0def __init__(self, name, age):self.name = nameself.age = agePerson.counter += 1tom = Person(name='Tom', age=19)
tom.counter
# 1
jay = Person(name='jay', age=20)
tom.counter
# 2
jay.counter
# 2
Person.counter
# 2
注意:我們將 counter
屬性的定義放在構造函數外面,可以看到,該變量在所有實例中共享同一份內存地址。
定義類方法需要使用到裝飾器 @classmethod,可以理解為將函數裝飾成類方法。類似于實例方法,類方法的第一個參數表示的是類,一般用 cls
來表示,當然你也可以使用其他名稱。
class Person:counter = 0def __init__(self, name, age):self.name = nameself.age = agePerson.counter += 1@classmethoddef number(cls):return cls.countertom = Person(name='Tom', age=19)
print(tom.number())
# 1
jay = Person(name='jay', age=20)
print(Person.number())
# 2
靜態方法
靜態方法使用裝飾器 @staticmethod 來聲明,其行為與普通函數一樣,只是放在了類的內部定義,使用方法與類方法一樣。
class Person:counter = 0def __init__(self, name, age):self.name = nameself.age = agePerson.counter += 1@staticmethoddef get_count():return Person.countertom = Person(name='Tom', age=19)
print(tom.get_count())
# 1
jay = Person(name='jay', age=20)
Person.get_count()
# 2
一般實例屬性和方法比較常用,其定義與外部定義也非常類似,實例屬性相當于在類內部環境中的全局變量,實例方法和類方法都只是固定了第一個參數的指向。使用起來也是比較簡單的,基本都可以通過實例來進行調用,而我們編程時主要也是面向實例對象。
__slots__
屬性
Python
是一門動態語言,允許我們在程序運行時為對象綁定新的屬性和方法,也可以將已有的屬性和方法進行解綁,如果我們想要限制自定義類的成員,可以通過 __slots__
屬性進行限定。
class Person:def __init__(self, name, age):self.name = nameself.age = ageclass PersonSlots:__slots__ = ('name', 'age')def __init__(self, name, age):self.name = nameself.age = ageps = PersonSlots('Tom', 20)
ps.name
# 'Tom'
ps.sex = 'female'
# AttributeError: 'PersonSlots' object has no attribute 'sex'
vars(ps)
# TypeError: vars() argument must have __dict__ attribute
為什么要使用 __slots__
屬性呢?主要有兩點好處
-
節省內存:不會創建動態數據結構
__dict__
來存儲屬性p = Person('Jay', 19) p.__dict__ # 沒有 __slots__ 屬性 # {'name': 'Jay', 'age': 19} ps.__slots__ # 沒有 __dict__ 屬性 # ('name', 'age')
-
提高屬性訪問速度,
__slots__
是靜態數據結構,因此無法添加新的屬性%timeit p.name # 52.8 ns ± 1.08 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each) %timeit ps.name # 45.9 ns ± 1.72 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
繼承與多態
單繼承
類繼承可以擴展現有類的功能,例如,我們定義一個 Person
類
class Person:def __init__(self, name, age):self.name = nameself.age = agedef say_hello(self):print('Hello', self.name)
并定義一個 Employee
類繼承自 Person
class Employee(Person):def __init__(self, name, age, job_title):super().__init__(name, age)self.job_title = job_title
我們使用 super
函數來訪問父類的構造函數,并使用父類的構造函數來初始化 name
和 age
屬性,并添加一個新的屬性 job_title
。
tom = Employee(name='Tom', age=19, job_title='Developer')
tom.say_hello()
# Hello Tom
isinstance(tom, Person)
# True
isinstance(tom, Employee)
# True
issubclass(Employee, Person)
# True
issubclass(Person, object)
# TRUE
所有類都隱式地繼承自頂層 object
類。子類會繼承父類的屬性和方法,定義與父類同名的屬性或方法相當于對其重寫
class Employee(Person):def __init__(self, name, age, job_title):super().__init__(name, age)self.job_title = job_titledef say_hello(self):super().say_hello()print('Employee :', self.name)tom = Employee(name='Tom', age=19, job_title='Developer')
tom.say_hello()
# Hello Tom
# Employee : Tom
多繼承
Python
允許多重繼承,即子類可以繼承自多個父類
class A:passclass B:passclass C(A, B):pass
繼承自多個類,不可避免的一個問題就是多個父類之間存在同名屬性和方法的問題,Python
中使用方法解析順序(MRO
,Method Resolution Order
)來解決這一問題,其核心是 C3
算法。可以使用 mro
函數來獲取類的搜索順序
C.mro()
# [__main__.C, __main__.A, __main__.B, object]
從左至右依次掃描類中是否存在搜索的屬性或方法,找到之后便直接執行并不再繼續搜索。例如
class Vehicle:def __init__(self, name, weight):self.name = nameself.weight = weightdef start(self):print("Let's go go go!")def accelerate(self):print('Run faster!')class Flyable:def __init__(self, wing=True):self.wing = wingdef fly(self):print('I can fly')def accelerate(self):print('Fly faster!')class Spaceship(Vehicle, Flyable):def __init__(self, name, weight, wing):super().__init__(name, weight)self.wing = wing
Spaceship
類繼承自 Vehicle
和 Flyable
,注意這兩個順序,使用 __mro__
屬性也可以查看搜索列表
Spaceship.__mro__
# (__main__.Spaceship, __main__.Vehicle, __main__.Flyable, object)
因此,在使用 super
調用父類的構造函數時,先找到的是 Vehicle
s = Spaceship('Varyag', 1000, False)
s.fly()
# I can fly
s.accelerate()
# Run faster!
s.start()
# Let's go go go!
多態
多態能夠使不同對象對同一消息做出不同的響應。例如
class Person: def speak(self):passclass Chinese(Person):def speak(self):print('I can speak Chinese')class American(Person):def speak(self):print('I can speak English')def speak(obj):obj.speak()
我們定義兩個繼承自 Person
的類,并分別實現 speak
方法,并定義一個函數,傳入一個對象,并調用對象的 speak
方法
c = Chinese()
a = American()
speak(a)
# I can speak English
speak(c)
# I can speak Chinese
但是這里會有一個問題,由于 Python
是動態語言不會檢查對象的類型,因此我們只要為 speak
函數傳入的對象含有 speak
方法,該函數就會正常運行。例如
class Pretenders:def speak(self):print('Pretenders')p = Pretenders()
speak(p)
# Pretenders
這也被稱為鴨子類型,即當一只鳥不管是走路、游泳還是叫起來都與鴨子很像,那么這只鳥就可以被稱為鴨子。在這里,不需要關注對象的類型,只要保證它們具有相同的行為即可。
封裝
封裝是為了隱藏程序某一部分的實現細節,在程序外部不可見,只將必要的接口暴露在外面。Python
對于類成員是沒有嚴格的訪問控制,默認情況下所有成員都是公開可訪問的。
在 Python
中私有化成員的方式也很簡單,只需將成員名稱以雙下劃線開頭的命名方式來聲明,也有人說使用單下劃線的方式來聲明私有成員,但這并不會影響成員的訪問,只能算是大家約定俗成的習慣。
偽私有成員
如果要設置私有屬性或私有方法,可以用
class Person:__count = 1def __init__(self, name, age):self.name = nameself.age = ageself.__number = Person.__countself.__increase()def __increase(self):Person.__count += 1def get_number(self):print(self.__number)
調用私有成員會拋出異常
tom = Person(name='Tom', age=19)
tom.__number
# AttributeError: 'Person' object has no attribute '__number'
tom.__increase()
# AttributeError: type object 'Person' has no attribute '__count'
Person.__count
# AttributeError: type object 'Person' has no attribute '__count'
tom.get_number()
# 1
jay = Person(name='jay', age=20)
jay.get_number()
# 2
這里說的私有成員并不是不可訪問,其實是 Python 將其變換了一個名稱,所以稱為偽私有成員。我們可以使用 __dict__
來查看實例對象和類的屬性
tom.__dict__
# {'name': 'Tom', 'age': 19, '_Person__number': 1}
tom._Person__number
# 1
Person.__dict__
# mappingproxy({'__module__': '__main__',
# '_Person__count': 3,
# '__init__': <function __main__.Person.__init__(self, name, age)>,
# '_Person__increase': <function __main__.Person.__increase(self)>,
# 'get_number': <function __main__.Person.get_number(self)>,
# '__dict__': <attribute '__dict__' of 'Person' objects>,
# '__weakref__': <attribute '__weakref__' of 'Person' objects>,
# '__doc__': None})
Person._Person__count
# 3
可以看到,私有成員都被重命名為“下劃線+類名+成員名”的方式。
特性
特性(property
)可以把一個特定屬性的訪問和賦值操作指向對應的函數或方法,使得我們能夠在屬性訪問和賦值時加入自動運行的代碼、并 攔截屬性刪除或為屬性提供文檔。
可以使用內置函數 property
為屬性添加訪問、賦值和刪除方法
class Person:def __init__(self, name):self._name = namedef get_name(self):print('get name')return self._namedef set_name(self, value):print('change name')self._name = valuedef del_name(self):print('delete name')del self._namename = property(fget=get_name, fset=set_name, fdel=del_name, doc="name property doc")tom = Person('Tom')
tom.name
# get name
# 'Tom'
tom.name = 'Robert'
# change name
tom.name
# get name
# 'Robert'
del tom.name
# delete name
使用裝飾器 @property
也可以達到相同的目的,包含三種使用方式
property
:將函數封裝為屬性,只可訪問,無法修改func.setter
:func
為被裝飾的函數,為其添加賦值方法func.deleter
:func
為被裝飾的函數,為其添加刪除方法
對于那些被封裝起來屬性,我們的本意可能是不希望用戶直接去修改,即使要修改,也要對新的值進行類型檢查,是否符合規則,例如
class Person:def __init__(self, name, age):self.__name = nameself.age = age@propertydef name(self):return self.__name@name.setterdef name(self, new_name):# 新的名稱必須為字符串且全部為英文字母if isinstance(new_name, str) and new_name.isalpha():self.__name = new_nameelse:print('invalid name')@name.deleterdef name(self):self.__name = ''
我們將 name
屬性封裝成私有屬性,并提供的 getter
、setter
和 deleter
方法,修改屬性值時,必須滿足條件才會成功執行,在刪除屬性時,我們僅僅將其賦值為空字符串
p = Person('Tom', 20)
p.name
# 'Tom'
p.name = 123
# invalid name
p.name = 'Jay'
p.name
# 'Jay'
del p.name
p.name
# ’‘
使用 @property
也可以將一個函數裝飾成一個特殊的計算屬性,讓函數的行為看起來是和屬性一樣
class Rectangle:def __init__(self, length, width):self.length = lengthself.width = width@propertydef area(self):return self.length * self.width@propertydef perimeter(self):return 2 * (self.length + self.width)r = Rectangle(10, 8)
r.area
# 80
r.length = 12
r.area, r.perimeter
# (96, 40)
當我們想直接修改或刪除 property
屬性時,會拋出異常。當然,我們也不會去為計算屬性進行賦值
r.area = 10
# AttributeError: can't set attribute
del r.perimeter
# AttributeError: can't delete attribute
運算符重載
何為運算符重載,即在自定義類中攔截內置的操作,當對自定義類的實例執行了內置操作,會自動調用你所定義的對應的魔法方法,使自定義類的行為看起來像是內置類。
我們前面介紹了一個魔法方法 __init__
是專門用于定義類的構造函數,什么是魔法方法,就是為類綁定的特殊方法,可以為自定義類添加一些額外的功能(如,獲取長度、切片、算術和邏輯運算等所有內置對象能做的事),它們都是以雙下劃線開頭和結尾的方法。
常見的運算符重載方法
方法 | 功能 |
---|---|
__new__ | 創建類實例的靜態方法,在構造函數之前被調用 |
__init__ | 構造函數 |
__del__ | 析構函數 |
__repr__ 、__str__ | 打印及字符串轉換 |
__call__ | 函數調用 |
__len__ | 計算長度 |
__bool__ | 布爾測試 |
__contains__ | 成員關系測試 |
__getattr__ | 點號運算 |
__getattribute__ 、__setattr__ 、__delattr__ | 屬性的獲取、設置、刪除 |
__getitem__ 、__setitem__ 、__delitem__ | 索引與切片、賦值和刪除 |
__iter__ 、__next__ | 迭代 |
__enter__ 、__exit__ | 上下文管理器 |
__lt__ 、__gt__ 、__le__ 、__ge__ 、__eq__ 、__ne__ | 比較運算 |
__add__ 、__sub__ 、__mul__ 、__true__div__ 等 | 算術運算 |
__and__ 、__or__ 、__xor__ | 邏輯運算 |
如果沒有定義相應的運算符重載方法,大多數內置函數都無法應用到類實例上
屬性引用
當我們訪問屬性時,有兩個方法會被調用:__getattr__
和 __getattribute__
,兩者之間的區別在于,不論訪問的對象屬性是否存在,都會首先執行 __getattribute__
,而 __getattr__
是點運算法攔截器,是屬性訪問的最后一道防線,如果屬性不存在將會拋出異常
class Attribute:def __init__(self, name):self.name = namedef __getattribute__(self, attr):print('getattribute')return object.__getattribute__(self, attr)def __getattr__(self, attr):print('getattr')raise AttributeError(attr + " con't access!")a = Attribute('Tom')
print(a.name)
# getattribute
# Tom
a.age
# getattribute
# getattr
# AttributeError: age con't access!
在 __getattribute__
方法中,我們調用了父類 object
中相應的方法,直接返回屬性值。
注意:任何對屬性的訪問(包括方法)都會調用 __getattribute__
方法,因此在自定義該方法時這很容易造成遞歸調用,例如
class Attribute:def __init__(self, name):self.name = namedef __getattribute__(self, attr):print('getattribute')if attr.lower() == 'name':return object.__getattribute__(self, attr)else:return self.other()def other(self):print('other')a = Attribute('Tom')
a.age
# RecursionError: maximum recursion depth exceeded while calling a Python object
在訪問屬性的小寫形式不是 name
時,將會調用 other
方法,但是訪問 other
方法之前會優先進入 __getattribute__
,導致遞歸調用
__setattr__
是屬性賦值的攔截器,所有試圖對屬性賦值的操作都會調用 __setattr__
方法,當我們需要定義該函數時,便不能直接使用 self.attr = value
,而需要用到我們前面提到的內置屬性 __dict__
,或者調用父類的 __setattr__
方法
class Attribute:def __init__(self, name, age):self.name = nameself.age = agedef __setattr__(self, attr, value):print('Set value')self.__dict__[attr] = valuea = Attribute('Tom', 19)
# Set value
# Set value
a.age
# 19
可以看到,在構造函數內部的賦值也會調用 __setattr__
方法。在重載該方法時需要謹慎,如果不把屬性添加到 __dict__
中,將會導致屬性不可用
class Attribute:def __init__(self, name, age):self.name = nameself.age = agedef __setattr__(self, attr, value):print('Set value')if attr in self.__dict__:self.__dict__[attr] = valueelse:print('Pass')a = Attribute('Tom', 19)
# Set value
# Pass
# Set value
# Pass
屬性的訪問也可以使用內置函數 getattr
,相當于點運算,還可以設置屬性不存在時返回的默認值,或拋出異常,而 內置函數 setattr
可用于設置屬性值, hasattr
可以用來判斷對象是否存在某一屬性
class Attribute:def __init__(self, name, age):self.name = nameself.age = agea = Attribute('Tom', 19)
getattr(a, 'name')
# 'Tom'
getattr(a, 'sex')
# AttributeError: 'Attribute' object has no attribute 'sex'
getattr(a, 'sex', 'female')
# 'female'
hasattr(a, 'sex')
# False
hasattr(a, 'age')
# True
setattr(a, 'sex', 'female')
hasattr(a, 'sex')
# True
getattr(a, 'sex')
# 'female'
索引和分片
對于實例的索引運算,會自動調用 __getitem__
方法,而 __ setitem__
主要用于修改對應索引處的值,__delitem__
可以刪除指定索引處的值。例如
class Container:def __init__(self, data):self.data = datadef __getitem__(self, index):print('Get value')return self.data[index]def __setitem__(self, index, value):print('Set value')self.data[index] = valuedef __delitem__(self, index):print('Delete value')del self.data[index]c = Container([48, 52, 1.08, 7, 1000, 124, 7])
c[1]
# Get value
# 52
c[1::2]
# Get value
# [52, 7, 124]
c[4] = -1
# Set value
c[4]
# Get value
# -1
del c[0]
# Delete value
c[0]
# Get value
# 52
__getitem__
方法也可以讓實例對象具有迭代功能,for
循環每次循環時都會調用類的 __getitem__
方法,并持續添加更高的偏移量
class Container:def __init__(self, data):self.data = datadef __getitem__(self, index):return self.data[index]c = Container([48, 52, 1.08, 7, 1000, 124, 7])
for i in c:print(i, end=' ')
# 48 52 1.08 7 1000 124 7
而任何支持 for
循環的類也會自動支持 Python
所有的迭代環境,如成員關系、列表解析等
'a' in c
# False
[i // 2 for i in c]
# [24, 26, 0.0, 3, 500, 62, 3]
a, b, c, *_ = c
a, b, d
# (48, 52, 7)
_
# [1.08, 7, 1000, 124]
迭代器
盡管 __getitem__
也可以支持迭代,但它只是一個退而求其次的方法,Python
中所有迭代都會優先嘗試 __iter__
方法,在其未定義的情況下,才會嘗試 __getitem__
。
迭代是通過內置函數 iter
去搜索 __iter__
方法,該方法會返回一個迭代器對象(實現了 __next__
方法的對象),然后重復調用該迭代器對象的 next 方法來不斷獲取值,直到發生 StopIteration
異常。
class Fibonacci:def __init__(self, n):self.n = nself.a, self.b = 0, 1def __iter__(self):return selfdef __next__(self):tmp = self.aif self.a > self.n:raise StopIterationself.a, self.b = self.b, self.a + self.breturn tmp
在這里迭代器對象就是 self
,在斐波那契值大于給定值時,使用 raise
來拋出異常,表示迭代結束
for i in Fibonacci(1000):print(i, end=' ')
# 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
f = Fibonacci(10)
f.__next__()
# 0
next(f)
# 1
...
next(f)
# StopIteration:
內置函數 next
與 f.__next__
是等同的,當然,上面的例子改成生成器會更簡單
def fib(n):a, b = 0, 1while a < n:yield aa, b = b, a+b
f = Fibonacci(10)
for i in fib(10):print(i, end=' ')
# 0 1 1 2 3 5 8
多個迭代器對象
迭代器也可以是一個獨立的類,保存其自己的狀態信息,從而允許為相同數據創建多個迭代器,例如
class AlphaIterator:def __init__(self, alpha):self.alpha = alphaself.offset = 0def __next__(self):if self.offset >= len(self.alpha):raise StopIterationvalue = self.alpha[self.offset]self.offset += 1return valueclass Alpha:def __init__(self, alpha):self.alpha = alphadef __iter__(self):return AlphaIterator(self.alpha)
我們定義了一個用于遍歷字符串的迭代器 AlphaIterator
,而在 Alpha
中不再返回其自身,因為其未定義 __next__
方法,并不是一個可迭代對象。
alpha = Alpha('ABCD')
alpha_iter = iter(alpha)
next(alpha_iter), next(alpha_iter)
# ('A', 'B')
我們使用 iter
函數來獲取 alpha
中的可迭代對象,然后使用 next
獲取迭代器的值
for i in alpha:for j in alpha:print(i + j, end=' ')
# AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD
每個循環都會獲取獨立的迭代器對象來記錄自己的狀態信息
成員關系
成員關系 in
通常被實現為一個迭代,即 __iter__
或 __getitem__
可以支持成員運算,如果要添加特定的成員關系,可以使用 __contains__
將成員關系定義為一個特定的映射關系或序列搜索方法。該方法優先于 __iter__
,而 __iter__
優先于 __getitem__
class Fibonacci:def __init__(self, n):self.n = nself.a, self.b = 0, 1self.seq = []def __contains__(self, value):print('contains: ')return value in self.seqdef __iter__(self):return selfdef __next__(self):print('next', end=' ')tmp = self.aif self.a > self.n:raise StopIterationself.a, self.b = self.b, self.a + self.bself.seq.append(tmp)return tmpdef __getitem__(self, index):print('get', end=' ')return self.seq[index]
測試執行順序,由于該類是一個迭代器,因此需要先獲取值
f = Fibonacci(1000)
for i in f:print(i, end=' ')
# next 0 next 1 next 1 next 2 next 3 next 5 next 8 next 13 next 21 next 34 next 55 next 89 next 144 next 233 next 377 next 610 next 987 next
587 in f
# contains:
# False
89 in f
# contains:
# True
當我們注釋掉 __contains__
方法后
f = Fibonacci(1000)
587 in f
# next next next next next next next next next next next next next next next next next next
# False
89 in f
# next
# False
f = Fibonacci(1000)
89 in f
# next next next next next next next next next next next next
# True
可以看到,調用迭代器不斷去尋找需要判斷的值,當我們再次測試時,由于迭代器已經遍歷完,所以找不到值,需要重新創建對象
可以看到, __getitem__
是沒有用到的,優先級最低
class Get:def __init__(self, data):self.data = datadef __getitem__(self, index):print('Get', end=' ')return self.data[index]g = Get([0, 1, 1, 2, 3, 5, 8])
4 in g
# Get Get Get Get Get Get Get Get
# False
5 in g
# Get Get Get Get Get Get
# True
字符串轉換
當我們創建自定義類時,希望在打印類對象時能夠有更好的展現形式,而不是輸出一串地址,這里需要用到 __str__
(用于打印和調用 str
內置函數時 的輸出)和 __repr__
(用于其他環境)。
兩者之間的區別在于,__repr__
可用于任何地方,當定義了 __str__
時,print 和 str 函數會優先使用該方法,如果沒有定義,在打印時會使用 __repr__
,反之并不成立
class AddStr:def __init__(self, value):self.value = valuedef __str__(self):return '[data]: {}'.format(self.value)a = AddStr(10)
a
# <__main__.AddStr at 0x7f55d2335a00>
print(a)
# [data]: 10
str(a)
# '[data]: 10'
class AddRepr:def __init__(self, value):self.value = valuedef __repr__(self):return '[data = {}]'.format(self.value)a = AddRepr(5)
a
# [data = 5]
print(a)
# [data = 5]
str(a)
# '[data = 5]'
class AddBoth:def __init__(self, value):self.value = valuedef __repr__(self):return '[data = {}]'.format(self.value)def __str__(self):return '[data]: {}'.format(self.value)a = AddBoth(123)
a
# [data = 123]
print(a)
# [data]: 123
str(a)
# '[data]: 123'
右側運算與原地運算
二元算術運算符都有右側運算和原地運算,所謂右側運算即當實例對象在運算符右側時調用的方法,一般只有在要求運算符具有交換性質時才會用到。例如
class Number:def __init__(self, num):self.number = numdef __mul__(self, other):print('mul')if isinstance(other, Number):other = other.numberreturn Number(self.number * other)def __rmul__(self, other):print('rmul')if isinstance(other, Number):other = other.numberreturn Number(other * self.number)def __imul__(self, other):print('imul')if isinstance(other, Number):other = other.numberself.number *= otherreturn selfdef __repr__(self):return "<Number>: %s" % self.numbera = Number(10)
b = Number(5)
a * b
# mul
# <Number>: 50
3 * a # 右側乘法,調用 __rmul__
# rmul
# <Number>: 30
a *= 2
# imul
a
# <Number>: 20
對象在運算符右側,需要定義對應 r
開頭(__rmul__
)的方法,原地運算可以實現以 i
開頭(__imul__
)的方法,如果沒有定義則會調用未加前綴(__mul__
)的方法
可調用對象
__call__
方法用于重載類對象的括號運算符,可以讓實例對象像普通函數一樣可調用
class ChangeColor:def __init__(self, colors):self.colors = colorsdef __call__(self, index):return self.colors[index]c = ChangeColor(colors=['blue', 'yellow', 'red', 'green'])
c(2)
# 'red'
c(0)
# 'blue'
判斷對象是否為可調用對象,可以使用內置函數 hasattr
來判斷是否存在 __call__
屬性
hasattr(c.colors, '__call__')
# False
hasattr(c, '__call__')
# True
hasattr(hasattr, '__call__') # 內置函數
# True
或者更簡便的 callable
函數
callable(c)
# True
callable(c.colors)
# False
callable(hasattr)
# True
該方法常見于 API
接口函數,例如我們定義一個按鈕用于切換顏色,并定義一個顏色類存儲顏色屬性并記錄狀態信息,并將切換顏色作為一個回調函數
class Colors:def __init__(self, colors):self.colors = colorsself.index = Truedef __call__(self):self.index = not self.indexprint(self.colors[self.index])class Button:def __init__(self, callback):self.callback = callbackdef click(self):# do somethingself.callback()c = Colors(colors=['blue', 'yellow'])
b = Button(c)
b.click()
# blue
b.click()
# yellow
比較運算
一般在需要對實例對象進行排序時會定義相應的比較運算, Python
中有 6
鐘比較運算:>
、<
、>=
、<=
、==
、!=
,不同于二元算術運算符,比較運算沒有右端形式,對于大于(大于等于)或小于(小于等于)操作,如果只定義其中一個,那么另一個運算會相應的進行取反操作,例如
class Person:def __init__(self, name, age):self.name = nameself.age = agedef __le__(self, other):return self.age <= other.agedef __lt__(self, other):return self.age < other.agetom = Person('Tom', 19)
jay = Person('Jay', 20)
tom >= jay, tom > jay
# (False, False)
tom <= jay, tom < jay
# (True, True)
但是對于相等與不等操作卻有些區別
class Person:def __init__(self, name, age):self.name = nameself.age = agedef __eq__(self, other):print('equal', end=' ')return self.age == other.agetom = Person('Tom', 19)
jay = Person('Jay', 20)
lux = Person('Lux', 19)
tom == lux, tom != jay
# equal equal
# (True, True)
當只定義 __eq__
方法時,不相等操作會將該函數的結果取反,該函數調用兩次
class Person:def __init__(self, name, age):self.name = nameself.age = agedef __ne__(self, other):print('not equal', end=' ')return self.age != other.agetom = Person('Tom', 19)
jay = Person('Jay', 20)
lux = Person('Lux', 19)
tom == lux, tom != jay
# not equal
# (False, True)
tom == tom
# True
可以看到,當只定義了 __ne__
時,判斷相等并不是對其取反,而且該方法只調用了一次,猜測可能調用了內置的 is
來判斷相等
布爾測試
當我們對一個自定義類的實例對象進行布爾判斷時,默認是 True
,可以定義 __bool__
方法來測試對象布爾值
class String:def __init__(self, data):self.data = datadef __bool__(self):return isinstance(self.data, str)s = String(123)
bool(s)
# False
s = String('aaa')
bool(s)
# True
如果沒有定義 __bool__
方法,則會退而求其次尋找 __len__
方法,當該方法返回 0
時,則對象為假
class String:def __init__(self, data):self.data = datadef __len__(self):return len(self.data) > 0s = String('aaa')
bool(s)
# True
s = String('')
bool(s)
# False
上下文管理器
上下文管理器用于在某些語句的上下文執行一段代碼,可以在運行這部分代碼之前,進行一些預處理,以及在執行完代碼后做一些清理工作。例如,對文件的讀寫操作,需要在退出前關閉文件,對數據庫的讀寫,需要先連接數據庫,讀寫完成之后需要關閉數據庫連接。
上下文管理器使用兩個方法來定義:
__enter__
:運行代碼之前調用該方法__exit__
:運行代碼之后或者代碼出現異常時調用該方法,接受額外的三個參數,分別代表:異常類型、異常內容、異常位置,當代碼塊未發生異常時這些參數的值都為None
import sqlite3class Fruits:def __init__(self, db):self.db = dbself.conn = Nonedef __enter__(self):print('Connect to %s' % self.db)self.conn = sqlite3.connect(self.db)return selfdef __exit__(self, exc_type, exc_val, exc_tb):if exc_type:passelse:print('Success')self.conn.close()self.conn = Nonedef create(self):print('Create database')cur = self.conn.cursor()# Create tablecur.execute('''CREATE TABLE stocks(date text, trans text, item text, count real, price real)''')# Insert a row of datacur.execute("INSERT INTO stocks VALUES ('2006-01-05','BUY','banana',12, 15.14)")cur.execute("INSERT INTO stocks VALUES ('2006-03-28', 'BUY', 'apple', 50, 45.0)")cur.execute("INSERT INTO stocks VALUES ('2006-04-06', 'SELL', 'cherry', 10, 53.0)")cur.execute("INSERT INTO stocks VALUES ('2006-04-05', 'BUY', 'watermelon', 3, 42.0)")# Save (commit) the changesself.conn.commit()def query(self):cur = self.conn.cursor()# queryfor row in cur.execute('SELECT * FROM stocks ORDER BY price'):print(row)
我們定義了一個操作數據庫的類,只需傳入一個數據庫文件名,我們導入了標準庫 sqlite3
用于操作數據庫,并定義了兩個方法用于創建和查詢數據庫,可以不用關心這兩個方法的實現,知道它們的功能即可。
上下文管理器主要使用 with...as
語句來調用
with Fruits('fruits.db') as db:db.create()db.query()
# Connect to fruits.db
# Create database
# ('2006-01-05', 'BUY', 'banana', 12.0, 15.14)
# ('2006-04-05', 'BUY', 'watermelon', 3.0, 42.0)
# ('2006-03-28', 'BUY', 'apple', 50.0, 45.0)
# ('2006-04-06', 'SELL', 'cherry', 10.0, 53.0)
# Success
可以看到,在執行 with...as
代碼塊的前后分別執行了 __enter__
和 __exit__
下面幾節都是類的高級話題,可以跳過,有需求的讀者可以繼續閱讀。