目錄
- 一、基本知識點
- 二、類的結構
- 三、類的執行個體化
- 四、類的繼承
- 五、抽象類
- 六、類的封裝功能
- 七、類中的方法
- 八、類的内置方法(特殊方法,後補)
- 九、對象執行個體化過程(簡單)
- 十、元類
- 十一、幾個技術問題
- 視角聚焦于解決問題的步驟,特點是每一步的行為都基本固定,且強烈依賴于上下文。一旦程式編碼完畢,程式内部各步驟代碼就形成一個強相關的整體,不友善頻繁的修改和擴充。
- 這種編碼形式在編碼前會明确解決問題的各個步驟并分解至最簡單的語句,故編碼較為簡單。
- 面向過程的程式代碼一般适用于不會經常變動代碼内容的場景,或者是在程式中的某一個部分使用面向過程編碼,這樣即使發生了需求變動,也隻需要重寫這一部分較少的代碼。
-
[x] 面向過程程式設計标簽
- [x] 簡單
- [x] 固定
- [x] 依賴上下文(耦合強)
- [x] 重寫
- [x] 适合小場景
- 站在上帝視角,對全局設計和模組化。
- 上帝創造出程式中的對象,不同對象有着獨特的屬性、特征、功能,對象之間互相作用一起支撐程式的運作。
- 因程式的運作完全來源于對象之間的互動,是以上帝也無法明确程式的每一個步驟。
- 面向對象程式設計的代碼一般适用于經常變動代碼内容的場景,通過此種程式設計方式産出的代碼,子產品間不會有很強的耦合性,這就提供了可以靈活更換、修改、擴充子產品的能力。
-
[x] 面向對象程式設計标簽
- [x] 複雜
- [x] 靈活
- [x] 子產品化(低耦合)
- [x] 可插拔
- [x] 适合大規模
- 面向過程和面向對象隻是對一個程式的設計視角和模式不同,兩者各有優缺點,并沒有哪一種是絕對的優秀而要抛棄另一種。
- 我覺得良好的程式設計應該是在合理的場景使用合理的程式設計模式,一個優秀的程式應該是可以支援混合程式設計,在程式代碼的不同階段、不同角度、不同抽象層次使用對應最适合的程式設計模式,各種程式設計模式相輔相成協作完成程式的正确執行。
- 不論是程式世界還是現實世界,類都是對于一個有着相似特征、屬性、功能的象集合體的描述。
-
一般我們以特征和功能來描述一個對象,如:
我們描述一個人,會說他有耳朵鼻子嘴巴四肢...,同時,他還可以開車、吃飯、聊天、看電影...
- 對象的長相,或者說特征(或者說可以被看到的屬性)是對象的描述之一。
- 對象的能力、功能、行為(對象可以做的事情)是對象的描述之二。
- 而多個相同對象應該有着相同或者相似的特征和功能(否則也不會被劃分成一個類别了),這些相似的屬性和功能就是這類對象的描述。
- 而反過來說,如果知道一類對象的描述,那麼上帝完全可以通過這些描述資訊,建立一個屬于此類的對象。
多個對象----->一個類别描述,這叫抽象
一個類别描述----->多個(一個)對象,這叫執行個體化
class Student(object, metaclass=type):
count = 0
def __init__(self, name, age):
self.name = name
self.age = age
def show_name(self):
print('my name is:', self.name)
類代碼可以暫時的了解成有如下幾個部分
- [x] 1、
關鍵字(必須)class
- [x] 2、類名
(必須)Student
- [ ] 3、繼承類清單
(可不寫)object
- [ ] 4、元類指定
metaclass=type
- [x] 3、類體(必須有,但可以是
)pass
- [ ] 以下為類體中可以包含的内容:
- [ ] 5、類資料屬性(可不寫)
- [ ] 6、構造函數
__init__
- [ ] 7、普通對象綁定方法
,也叫執行個體方法(可不寫)show_name
是以一個最最簡單的類代碼就像這樣
class Student:
pass
函數
: 1.
python
解釋器在編譯代碼的時候隻會在命名空間中聲明函數引用和函數對象的建立,不會執行函數體,函數體中的局部變量也不會被建立。
- 當函數被執行時,執行函數體中的代碼,局部命名空間被建立,局部變量随之建立。
類
python
解釋器在編譯代碼的時候會在命名空間聲明類引用和類對象的建立,會執行類體代碼,并建立此類的命名空間,命名空間作為類對象
的__dict__
屬性值。
- 如果類中的代碼是函數定義或者其他變量定義,則執行聲明定義,如果代碼是執行語句(如
)則執行此語句。print
- 當類被執行時,如:
,類不會執行類體代碼,而是傳回一個此類的執行個體化對象,傳回對象的過程叫做執行個體化。Student()
對象的屬性儲存在對象的__dict__字典中,可以通過 對象.屬性名 的方式來通路和修改。
類也是一個對象,類在定義時,類體中定義的資料即為類對象的屬性,可以通過 類名.資料名 的方式來通路和修改,如:
class Student:
a = 2
print(Student.a) # a = 2
Student.a = 44
print(Student.a) # a = 44
del Student.a
print(Student.a) # 報錯 AttributeError: type object 'Student' has no attribute 'a'
類中的方法就是一個普通的函數定義,此函數屬性也儲存在類對象的__dict__字典中,和資料屬性一樣,可以通過 類名.函數名 的方式來通路和修改,如:
def say(self):
print('全局的say方法, hi!')
class Student:
a = 2
def say(self):
print('類裡面的say方法, hello!')
Student.say('something') # 類裡面的say方法, hello!
# 類中函數屬性可以重新指派
Student.say = say
Student.say('something') # 全局的say方法, hi!
del Student.say
Student.say('something') # AttributeError: type object 'Student' has no attribute 'say'
注意:
1、類中定義的任何函數(預設情況下)都必須固定第一個self參數的存在,是以如果是以類為主體使用類中的函數時,就必須給self參數傳遞一個值,不論這個值是什麼。
2、類可以當做是一個容器對象來儲存資料。
類的另一個非常重要的作用是:執行個體化對象。
class Student:
school = '北京大學'
def __init__(self, name, age):
self.name = name
self.age = age
stu1 = Student('stu1', 26)
stu2 = Student('stu2', 22)
print(stu1) # <__main__.Student object at 0x000001F1E8E780B8>
print(stu2) # <__main__.Student object at 0x000001F1E8E780F0>
print(stu1.school) # 北京大學
print(stu2.school) # 北京大學
print(stu1.name, stu1.age) # stu1 26
print(stu2.name, stu2.age) # stu1 26
1、類可以被執行,執行時傳入的參數與 __init__
函數參數對應(不用傳self)
2、類執行的結果是一個對象,有記憶體位址
3、類執時傳入參數的目的是為了初始化這個執行個體化對象的某些屬性值(當然也可以不初始化,在後續配置)
4、類中的資料屬性,被所有此類對象共享 '北京大學'
5、類中的函數屬性,預設是綁定到對象的,即每個對象都有一個綁定方法,此方法的執行代碼指向類中的此函數
6、每一個對象都有自己的屬性值,通過 對象.屬性名 來通路和修改
7、類執行個體化的目的是為了得到一個對象,程式中有了對象之後就可以互相互動,完成程式的執行。
對象使用綁定方法
1、對象在建立時,python會将類中的普通方法做一些處理,然後綁定到對象身上。
2、對象在使用這些綁定方法的時候,會自動的将對象本身傳入到此方法的第一個self參數中,這樣就可以在函數中引用到此對象。
3、綁定方法是将函數和對象綁定在了一起,隻要綁定方法被調用,就會自動傳入對應的對象。
class Animal(object):
pass
class People(Animal):
pass
class Student(People):
pass
print(Student.mro())
print(Student.__mro__) # 二者等價,唯一的差別是上面結果是清單,下面是元組
# 傳回結果:
# [<class '__main__.Student'>, <class '__main__.People'>, <class '__main__.Animal'>, <class 'object'>]
# (<class '__main__.Student'>, <class '__main__.People'>, <class '__main__.Animal'>, <class 'object'>)
1、類可以繼承,即如果我們把多個類的相同之處再提取出來,就可以再次抽象出一個類,此類作為其所有子類的父類。
2、繼承就類似家族樹、學科專業目錄、動物類别,最頂層的是最抽象的類别,越往下走,類别越清晰,繼承樹的末端就是各個具體的對象。
3、我們可以使用MRO來表示一個類它向上方向的父類路徑,MRO是一個通過算法計算得到的父類元組。
4、任何一個類的MRO都可以表示它在繼承樹中的位置。
MRO的用處
MRO可以确定搜尋路徑
子類一旦繼承父類,就會自動繼承父類的所有代碼定義(實際并沒有直接得到,而是通過MRO搜尋得到)
繼承的好處是子類不需要重複編寫和父類相同的代碼,同時繼承也可以很明确的表示出什麼是什麼的結構關系,壞處是繼承使用的越多,這個繼承樹上的耦合性越強,因為一旦頂層類發生了變動,下方所有子類都會受到影響。
子類自動繼承父類的所有代碼。
子類可以在此基礎上新增自己的代碼,這叫派生,子類的代碼又會被自己的子類所繼承。
子類可以通過super()函數來調用上一級父類的屬性,注意,僅僅是向上一級,依賴于繼承樹。
子類也可以通過 父類名.屬性名 的方式來調用父類的屬性,不依賴于繼承樹。
子類也可以重寫覆寫父類的代碼,此時将會以子類提供的屬性值為準。
子類通過super()來調用上一級父類的屬性。
通過 父類名.屬性名 的方式調用任何一級父類的屬性。
當有多繼承,即有多個父類的情況下,super()函數的上一級父類是哪一個取決于MRO中的搜尋路徑。
函數内部變量的通路原則是:LEGB
函數局部---嵌套函數局部(如有嵌套)---全局空間---内置空間--報錯
對象通路一個屬性的原則是
對象---類
__dict__
---父類
__dict__
---基類object
__dict__
---元類
__dict__
---報錯
__dict__
class MyMeta(type):
a = 100
pass
class Animal(object):
pass
class Student(Animal, metaclass=MyMeta):
pass
print(Student.a) # a = 100
python2中的舊式類(即沒有繼承object的類及其子類)使用深度優先。
python2和python3的新式類使用廣度優先。
深度優先就是最長繼承路徑優先搜尋。
廣度優先,從左向右開始,搜尋到有共同父類的前一個類放棄目前搜尋路徑。
抽象類用于規定子類們相同功能的函數接口
抽象類提供抽象方法定義,但是并不實作
抽象類不能被執行個體化,隻能被繼承
抽象類由對多個有着相似屬性和功能的類進行抽象得到
子類一旦繼承抽象類,必須實作抽象類中定義的抽象方法
python自身沒有提供抽象類功能,需要使用abc子產品來提供支援
抽象類兼具接口和類的部分特性
抽象類的好處是,明确了類繼承的語義,且規範了子類的函數接口,提高了歸一化
import abc # 借助子產品實作抽象類功能
class Animal(object, metaclass=abc.ABCMeta):
@abc.abstractmethod # 定義接口
def run(self):
pass
@abc.abstractmethod
def sleep(self):
pass
class People(Animal):
def run(self):
print('running...') # 子類必須實作,且接口必須嚴格按照抽象類的定義
def sleep(self):
print('sleeping...')
p = People() # 隻有子類可以執行個體化
p.run()
p.sleep()
__xx
__xx
類的定義中,可以使用形如
__xx
的變量名來隐藏敏感資料,這些變量名會在編譯的時候變形成:
類名__xx
的形式存在。使用了這種變形功能後,可以提供一個唯一的資料通路和設定接口來控制敏感資料的通路。
這種變形是約定俗成的使用方式,實際上依然可以通過
類名__xx
的方式來通路資料。
這種變形方式對于資料和函數均可以使用。
class Student:
def __init__(self, name):
self.__name = name
def __show(self):
pass
stu1 = Student('stu1')
print(stu1.__dict__) # {'_Student__name': 'stu1'}
print(stu1.name) # AttributeError: 'Student' object has no attribute 'name'
property
的主要功能是提供一個僞裝,對外的接口是一個普通的屬性名,而在内部通過函數執行來通路和設定資料。
property
可以提供
getter
、
setter
deleter
三種資料的通路形式,内部函數可以用于對于資料的通路控制。
property
也可以用于需要實時執行計算的屬性,如三角形面積的計算、人的
BMI
指數的計算。
一般會将
__xx
和
property
聯合使用,因為property需要使用另一個屬性名來防止無限遞歸。
class Student:
def __init__(self, name, money):
self.name = name
self.__money = money # 注意,需要使用另一個屬性名,否則會無限遞歸
@property
def money(self):
print(f'這裡可以控制{self.name}的money屬性通路')
return self.__money
@money.setter
def money(self, new_moeny):
print(f'這裡可以控制{self.name}的money屬性設定')
self.__money = new_moeny
@money.deleter
def money(self):
print(f'這裡可以控制{self.name}的money屬性删除')
raise AttributeError('此屬性不可删除')
stu1 = Student('stu1', 30000)
print(stu1.money)
stu1.money = 40000
del stu1.money
函數封裝一般用于隐藏内部實作細節,提供公開統一接口。
隐藏内部細節函數可以提神安全性,因為一旦公開細節函數,就要考慮會被外部使用者調用。
應該将一個功能封裝成一個公開的接口,對開開放,同時此接口需要做一定的通路控制。
**可以使用__xx的方式來隐藏細節函數,一般在類中也會使用_函數名的方式來表示此函數是内部函數。**
class Student:
def __init__(self, name, money, password):
self.name = name
self.__money = money
self.__password = password
def show_money(self, password): # 對外僅僅提供這個公開接口
print('這裡可以控制通路此函數的權限')
if password == self.__password: # 通路控制處理
return self.__get_money()
else:
print('拒絕通路')
return None
def __get_money(self): # 内部實作細節函數,一般無法直接通路
return self.__money
stu1 = Student('stu1', 30000, '123')
print(stu1.show_money('123')) # 驗證正确,得到敏感資料
print(stu1.show_money('abc')) # 沒有得到敏感資料
類中定義的函數在預設情況下就是執行個體綁定方法。在執行個體化對象的過程中,
python
會将函數與對象綁定形成一個綁定方法。當綁定方法被調用時,會自動傳遞對象作為第一個
self
參數。
類中定義的函數增加了
@classmethod
裝飾器之後将會被定義成類綁定方法,和執行個體綁定方法類似的,類綁定方法将會把函數與類對象綁定在一起,當類綁定方法被調用的時候,會自動傳遞類對象作為第一個
cls
類中使用
@staticmethod
裝飾器的函數,此時函數作為一個普通的函數存在于類空間中,在使用時必須嚴格按照普通函數的參數傳遞方式
1、通過類名執行調用,如:
Student()
2、
Student
類中的
__new__
方法被執行,将
Student
對象傳入作為第一個
cls
參數,此方法将會調用父類的
__new__
方法并傳回一個對象
obj
3、在
__new__
方法中,
Student
__init__
obj
self
參數,此方法傳回值固定為None
4、
__new__
方法傳回經過
__init__
函數初始化過的對象
obj
,
5、指派給變量
stu1 = Student()
exec是内置函數,和eval類似,可以執行字元串形式的python代碼
exec函數有3個參數:代碼、全局空間、局部空間
code = """
a = 2
global b
b = 3
def show():
print('hello')
"""
g_dic = {}
l_dic = {}
exec(code, g_dic, l_dic)
print(g_dic) # b的定義
print(l_dic) # a和show函數的定義
如果一切皆對象,那麼python的類也是對象。類對象是如何産生的?
python中的類對象通過元類産生,即:元類産生類對象,類對象再執行個體化對象。
python中的元類是type,元類産生了所有的python類,最重要的就是type類産了object類,即:通過元類的定義,可以定制object類的内容。
類名、繼承清單、類體代碼
元類的執行将會産一個類對象,類對象從元類的
__new__
函數産生,并經過元類的
__init__
初始化屬性。
class MyMeta(type):
def __new__(cls, class_name, class_bases, class_dic):
print('元類,cls is:', cls.__name__)
print('現在準備建立類對象:', class_name)
return super().__new__(cls, class_name, class_bases, class_dic)
def __init__(self, class_name, class_bases, class_dic):
print('選擇要對類對象初始化', self.__name__)
self.class_name = class_name
self.class_bases = class_bases
self.class_dic = class_dic
self.a = 2
class Student(object, metaclass=MyMeta):
pass
print(Student.a) # 2
元類調用new方法的時候傳入的是類的三元素,并傳回一個類對象,類對象被傳入init方法中,并對此類對象進行初始化
class MyMeta(type):
def __call__(self, *args, **kwargs):
print('此類正在執行call', self.__name__)
obj = object.__new__(self)
self.__init__(obj, *args, **kwargs)
return obj
class Student(object, metaclass=MyMeta):
pass
stu1 = Student()
print(stu1)
類對象在執行個體化的時候,會調用方法(此方法應該是元類給予的),
call
方法将會調用
call
的
object
方法得到一個空對象,然後對此空對象進行初始化,并傳回此對象。
new
class Student:
__instance = None
def __new__(cls, *args, **kwargs):
if cls.__instance is None:
obj = object.__new__(cls)
cls.__init__(obj, *args, **kwargs)
cls.__instance = obj
return cls.__instance
stu1 = Student()
stu2 = Student()
print(stu1 is stu2) # True
class MyMeta(type):
def __init__(self, *args, **kwargs):
self.instance = None
def __call__(self, *args, **kwargs):
if self.instance is None:
obj = object.__new__(self)
self.__init__(obj, *args, **kwargs)
self.instance = obj
return self.instance
class Student(object, metaclass=MyMeta):
pass
stu1 = Student()
stu2 = Student()
print(stu1 is stu2)
個人覺得直接使用類的new操作更友善
方法是生成對象的方法,在元類中,使用
new
元類的
type
new
方法生成類對象。
在類中,使用
object
方法生成執行個體化對象。
new
方法應該是調用底層接口在記憶體中申請一個空間。
new
方法會傳回一個空對象。
new
方法是對對象進行初始化的方法,在元類中,是對類對象進行初始化,在類中,是對對象進行初始化。
init
方法傳回值是
init
。
None
class Animal:
def f(self):
print('這裡是animal')
print('self 是:', self)
class People(Animal):
def f(self):
print('這裡是people')
super().f()
class Student(People):
def f(self):
print('這裡是student')
super().f()
stu1 = Student()
print(stu1) # 和stu1.f()中的對象是同一個
stu1.f()
super()方法會将子類對象傳遞給父類
class Student:
def eat(self):
print('eating...')
stu1 = Student()
stu2 = Student()
print(stu1.eat) # bound method 0x000002C94A04C9E8
print(stu2.eat) # bound method 0x000002C94A04CA20
兩個對象,都是使用同一個函數,但是綁定方法位址卻不同
綁定方法也是對象,是對普通函數和對象的封裝
class Student:
def eat(self):
print('eating...')
stu1 = Student() # __main__.Student object 0x00000192233FC9E8
print(stu1)
bound_func = stu1.eat
print(bound_func.__self__) # __main__.Student object 0x00000192233FC9E8
print(bound_func.__func__) # function Student.eat at 0x00000192233F9488
1、綁定方法是一個對象
2、綁定方法将執行個體化對象和函數對象封裝在一起
3、綁定方法在調用的時候應該執行函數對象,并把執行個體化對象傳入給函數self參數
class Wrapper:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kw):
print('start')
self.func(*args, **kw)
print('end')
@Wrapper
def show():
print('這裡是show函數')
show()
# start
# 這裡是show函數
# end
class Wrapper:
def __init__(self, key):
self.key = key
def __call__(self, func):
self.func = func
def inner(*args, **kw):
print('start')
self.func(*args, **kw)
print('end')
return inner
@Wrapper('key')
def show():
print('這裡是show函數')
show()
# start
# 這裡是show函數
# end
class Student:
a = 2
print(getattr(Student, 'a')) # 2,getattr是擷取對象的屬性
dic = {
'a': 100,
}
print(getattr(dic, 'a')) # 報錯,字典中的key需要使用dic['a']通路