Python 入門 —— 面向對象編程

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__ 屬性呢?主要有兩點好處

  1. 節省內存:不會創建動態數據結構 __dict__ 來存儲屬性

    p = Person('Jay', 19)
    p.__dict__    # 沒有 __slots__ 屬性
    # {'name': 'Jay', 'age': 19}
    ps.__slots__  # 沒有 __dict__ 屬性
    # ('name', 'age')
    
  2. 提高屬性訪問速度,__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 函數來訪問父類的構造函數,并使用父類的構造函數來初始化 nameage 屬性,并添加一個新的屬性 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 中使用方法解析順序(MROMethod 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 類繼承自 VehicleFlyable,注意這兩個順序,使用 __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.setterfunc 為被裝飾的函數,為其添加賦值方法
  • func.deleterfunc 為被裝飾的函數,為其添加刪除方法

對于那些被封裝起來屬性,我們的本意可能是不希望用戶直接去修改,即使要修改,也要對新的值進行類型檢查,是否符合規則,例如

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 屬性封裝成私有屬性,并提供的 gettersetterdeleter 方法,修改屬性值時,必須滿足條件才會成功執行,在刪除屬性時,我們僅僅將其賦值為空字符串

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: 

內置函數 nextf.__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__

下面幾節都是類的高級話題,可以跳過,有需求的讀者可以繼續閱讀。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/35710.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/35710.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/35710.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

低代碼:釋放企業創新力的鑰匙

近年來&#xff0c;隨著信息技術的不斷發展&#xff0c;企業對于快速開發應用程序的需求越來越迫切。然而&#xff0c;傳統的軟件開發過程常常耗時費力&#xff0c;限制了企業的創新潛力。于是&#xff0c;低代碼應運而生&#xff0c;成為解決開發難題的一把利器。 低代碼開發…

你了解RabbitMQ、RocketMQ和Kafka嗎?

是的&#xff0c;我了解 RabbitMQ、RocketMQ 和 Kafka。以下是對這三種消息隊列系統的詳細介紹&#xff1a; RabbitMQ 概念 RabbitMQ 是一個由 Pivotal 開發的開源消息代理&#xff0c;基于 AMQP&#xff08;Advanced Message Queuing Protocol&#xff09;協議。它支持多種…

智能聊天AI機器人網頁怎么聊?這樣做很簡單

智能聊天AI機器人網頁怎么聊&#xff1f;隨著科技的飛速發展&#xff0c;智能聊天AI機器人已經逐漸滲透到我們的日常生活中&#xff0c;為我們提供了更加便捷、高效的交流方式。在網頁上&#xff0c;這些智能聊天機器人以其獨特的魅力&#xff0c;為我們打開了與機器對話的新世…

Epic商店登錄時一直轉圈圈怎么回事?Epic登錄轉圈圈解決辦法

很多游戲玩家都喜歡在Epic商店上面免費領取游戲&#xff0c;但是經常在登陸領取的過程中&#xff0c;遇到Epic賬號登陸不上的問題&#xff0c;登陸界面一直轉圈圈&#xff0c;下面分享一下具體解決辦法&#xff0c;幫助大家順利流暢登陸&#xff0c;輕松喜加一。 如果遇到Epic商…

低內阻、高性能數字音頻功放芯片-NTP8938

由工采網代理的韓國NF&#xff08;耐福&#xff09;NTP8938是一款支持2X30W低內阻、高性能數字音頻功放芯片&#xff1b;采用QFN40封裝&#xff0c;芯片內置DSP集成了多功能數字音頻信號處理功能&#xff0c;高性能&#xff0c;高保真。 芯片工作電壓范圍&#xff1a;5V&#x…

python實現可視化大屏(django+pyechars)

1.實現效果圖 2.對數據庫進行遷移 python manage.py makemigrations python manage.py migrate 3.登錄頁面 {% load static%} <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport"…

ffmpeg將mp4轉換為swf

文章目錄 ffmpeg安裝、配置java運行報錯 Cannot run program "ffmpeg" ffmpeg命令mp4轉為swf示例 ### ffmpeg -i input.mkv -b:v 600 -c:v libx264 -vf scale1920:1080 -crf 10 -ar 48000 -r 24 output.swfmkv轉為swf示例 其他文檔命令參數簡介 需要將mp4轉換為swf&a…

【回溯算法題記錄】組合總和題匯總

組合總和 39. 組合總和題目描述初始思路后續分析 40. 組合總和 II題目描述思路&#xff08;參考代碼隨想錄&#xff09; 39. 組合總和 題目&#x1f517; 題目描述 給你一個 無重復元素 的整數數組 candidates 和一個目標整數 target &#xff0c;找出 candidates 中可以使數…

3d渲染軟件有哪些(2),渲染100邀請碼1a12

3D渲染軟件有很多&#xff0c;上次我們介紹了幾個&#xff0c;這次我們接著介紹。 1、Arnold Arnold渲染器是一款基于物理算法的電影級渲染引擎&#xff0c;它具有渲染質量高、材質系統豐富、渲染速度快等特點&#xff0c;是3D設計師的極佳選擇。2、Octane Render Octane Ren…

[Gstreamer] gstbasesink 里的 jitter

gstbasesink 里有一個值是 jitter &#xff0c;直譯為抖動。這個值表示當前到達 gstbasesink chain 函數(push mode) 的 GstBuffer 的系統事件 與 這個 buffer 被期望到達的系統時間的差值。 如果 jitter 是整數&#xff0c;則表示 GstBuffer 到晚了&#xff0c;當前 GstBuffer…

HJI與HJB

問題描述 設連續系統狀態方程和性能指標 X ˙ f ( t , X , U ) X ( t 0 ) X 0 J ? [ X ( t f ) , t f ] ∫ t 0 t f F ( X , U , t ) d t \begin{aligned} \dot{X} & f(t, X, U) \quad X\left(t_{0}\right)X_{0} \\ J & \phi\left[X\left(t_{f}\right), t_{f}\r…

【全網最完整】Open CASCADE Technology (OCCT) 構建項目,QT可視化操作,添加自定義測試內容

前言 本文為了記錄自己在實習的過程中&#xff0c;學習到的有關OCCT開源項目的搭建工作&#xff0c;旨在教會小白從0開始下載開源項目及環境搭配&#xff0c;以及如何添加自定義測試內容&#xff0c;最終結果展示如下&#xff1a; 1、項目下載 本項目共需要使用四個工具&#…

如何快速解決驗證碼圖像問題 | 最佳圖像(OCR)驗證碼解決工具

你是否曾經遇到過陷入一個看似無盡的 CAPTCHA 挑戰中&#xff0c;努力識別扭曲的字符或數字&#xff1f;這些令人抓狂的 CAPTCHA 是為了確保你是人類而不是機器人&#xff0c;但它們也給真正的用戶帶來了頭痛。那么&#xff0c;有沒有快速解決這些 CAPTCHA 圖像的方法&#xff…

2021年12月電子學會青少年軟件編程 中小學生Python編程等級考試三級真題解析(判斷題)

2021年12月Python編程等級考試三級真題解析 判斷題&#xff08;共10題&#xff0c;每題2分&#xff0c;共20分&#xff09; 26、在Python中&#xff0c;0x100010表示十六進制數100010 答案&#xff1a;對 考點分析&#xff1a;考查進制轉換&#xff0c;十六進制數1??0x開頭…

Flask之數據庫

前言&#xff1a;本博客僅作記錄學習使用&#xff0c;部分圖片出自網絡&#xff0c;如有侵犯您的權益&#xff0c;請聯系刪除 目錄 一、數據庫的分類 1.1、SQL 1.2、NoSQL 1.3、如何選擇&#xff1f; 二、ORM魔法 三、使用Flask-SQLALchemy管理數據庫 3.1、連接數據庫服…

移動互聯網應用程序(APP)信息安全等級保護測評標準解讀

隨著移動互聯網的迅猛發展&#xff0c;移動應用(App)已成為個人信息處理與交互的主要渠道&#xff0c;其安全性直接關系到國家安全、社會穩定以及用戶個人隱私權益。為加強移動App的信息安全管理&#xff0c;國家標準化管理委員會正式發布了GB/T 42582-2023《信息安全技術 移動…

等保2.0時,最常見的挑戰是什么?

等保2.0的常見挑戰 等保2.0&#xff08;網絡安全等級保護2.0&#xff09;是中國網絡安全領域的基本制度&#xff0c;它對信息系統進行分級分類、安全保護和安全測評&#xff0c;以提高信息系統的安全性和可信性。在等保2.0的實施過程中&#xff0c;企業和組織面臨多方面的挑戰&…

寵物領養救助管理系帶萬字文檔java項目基于springboot+vue的寵物管理系統java課程設計java畢業設計

文章目錄 寵物領養救助管理系統一、項目演示二、項目介紹三、萬字項目文檔四、部分功能截圖五、部分代碼展示六、底部獲取項目源碼帶萬字文檔&#xff08;9.9&#xffe5;帶走&#xff09; 寵物領養救助管理系統 一、項目演示 寵物領養救助系統 二、項目介紹 基于springbootv…

一站式BI解決方案:從數據采集到處理分析,全面滿足決策支持需求

在數字化浪潮席卷全球的今天&#xff0c;數據已成為企業決策的核心驅動力。然而&#xff0c;面對海量的數據和復雜的分析需求&#xff0c;企業如何高效地收集、整理、分析和利用這些數據&#xff0c;以支持戰略決策和業務優化&#xff0c;成為了一個亟待解決的問題。為了解決這…

AI大模型日報#0626:首款大模型芯片挑戰英偉達、面壁智能李大海專訪、大模型測試題爆火LeCun點贊

導讀&#xff1a;AI大模型日報&#xff0c;爬蟲LLM自動生成&#xff0c;一文覽盡每日AI大模型要點資訊&#xff01;目前采用“文心一言”&#xff08;ERNIE-4.0-8K-latest&#xff09;生成了今日要點以及每條資訊的摘要。歡迎閱讀&#xff01;《AI大模型日報》今日要點&#xf…