第三章:深入類和對象

1.鴨子類型和多態(tài)

什么是鴨子類型
當(dāng)一只鳥走起路來像鴨子,游泳起來像鴨子,叫起來像鴨子,就可以稱這只鳥為鴨子.
也就是,當(dāng)我們有很多類都實現(xiàn)了同樣的方法,這些類就可以被看成是同一個種類.
Python中的變量本身是沒有類型的,所以它本身就代表著多態(tài),它可以表示成為任何的類型.

# encoding:utf-8
__author__ = 'Fioman'
__time__ = '2019/3/14 14:52'


class Cat(object):
    def say(self):
        print('我是一只貓')


class Dog(object):
    def say(self):
        print('我是一只狗')


class Duck(object):
    def say(self):
        print('我是一只鴨子')


animal_list = [Cat(),Dog(),Duck()]

for animal in animal_list:
    animal.say()

還有一個例子:list的extend方法
lst.extend(self ,iteralbe) 可以看到iterable就是一種鴨子類型,它代表的是一個可迭代的類型

a = [1,2]
b = [2,1]
c = (3,4)
d = dict(x=1,y=2) # 字典也是可迭代對象,但是默認(rèn)迭代是字典的鍵
e = set()
e.add(5)
e.add(6)

a.extend(b)
a.extend(c)
a.extend(d)
a.extend(e)
print(a)

output:
1, 2, 2, 1, 3, 4, 'x', 'y', 5, 6]

2.抽象基類(abc abstract class的縮寫)

什么是抽象基類
1. 如果有的類繼承了抽象基類,它就必須實現(xiàn)抽象基類中的所有的方法
2. 抽象基類是沒有辦法實例化的,就是通過它不能創(chuàng)建對象

既然Python是基于鴨子類型的,也就是說所有的類增加一些魔法方法就可以實現(xiàn)特殊的特性,它不需要像靜態(tài)語言那樣必須繼承某個類才能獲取一些特性.那為什么需要抽象基類呢?因為有時候,我們可以通過抽象基類來判斷某個是否屬于某種類型.

Python的collections.abc模塊有一些抽象基類,你可以根據(jù)isinstance(object,abcClass)來判定某個類是不是某種類型

如何去模擬一個抽象基類呢?
最簡單的方法是通過拋出異常的方式來實現(xiàn)

# 最簡單的方法就是在基類中拋出一個異常,但是這種方法有一個缺點.
# 就是在調(diào)用方法的時候才會拋出異常
class CacheBase():
    def get(self,key):
        raise NotImplementedError
    def set(self,key,value):
        raise NotImplementedError

class RedisCache(CacheBase):
    pass

redis_cache = RedisCache()
redis_cache.set('key','value')

這樣有一個缺點,就是在調(diào)用方法的時候才拋出異常,而在初始化的時候沒有問題,有沒有什么辦法可以在初始化的時候就可以拋出異常,告訴你必須實現(xiàn)抽象基類的方法呢.使用abc模塊中的@abc.abstractmethod裝飾器的方式可以實現(xiàn).

import abc
class CacheBase(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def get(self,key):
        pass

    @abc.abstractmethod
    def set(self,key,value):
        pass

class RedisCache(CacheBase):
    def get(self,key):
        pass
    def set(self,key,value):
        pass

cache = RedisCache()

3.isinstance和type的區(qū)別?

type只會判斷這個對象或者類的直屬對象,而isinstance可以向上追溯到它的父類.所有有時候type沒有isinstance準(zhǔn)確.

# encoding:utf-8
__author__ = 'Fioman'
__time__ = '2019/3/14 16:53'


class A:
    pass


class B(A):
    pass
b = B()
print(isinstance(b,B))
print(isinstance(b,A))
print(type(b) is B)
print(type(b) is A)

4. 類變量和實例變量

1. 類變量屬于類,也屬于實例.所有的實例對象和類共有一份類變量.并且類和實例都可以調(diào)用類變量
2. 實例變量屬于實例,不屬于類.實例變量是具體的實例單獨擁有.并且實例變量只能由具體的實例來調(diào)用.不能由類變量來調(diào)用.
3. 如果實例變量和類變量重名,則實例變量調(diào)用的就是自己的實例變量,如果通過實例對象對類變量進(jìn)行賦值,則它不會調(diào)用類變量,而是重新增加一個同名的實例屬性.這點是個坑,有點類似全局變量和局部變量的味道.所以我們在使用的時候,最好的習(xí)慣是使用類來調(diào)用類變量,而減少使用實例對象調(diào)用類變量的情況.其實這里如果用類來調(diào)用,就相當(dāng)于是加了一個global聲明,表示這個變量是類變量.而如果你用實例對象來調(diào)用的時候,如果是訪問,它就會向上查找,如果是修改,例如賦值,它就不會向上查找,如果沒有這個屬性,它就會直接創(chuàng)建

# encoding:utf-8
__author__ = 'Fioman'
__time__ = '2019/3/14 16:59'

class A:
    aa = 1 # 類變量
    def __init__(self,x,y):
        self.x = x  # 實例變量
        self.y = y  # 實例變量

a = A(2,3) # a是一個實例
A.aa = 11  # 類變量是所有的實例和類共有的變量,只有一份.
a.aa = 100 # 如果創(chuàng)建的實例變量和類變量重合,則實例變量就多出來了一個屬性aa
           # 這樣在調(diào)用的時候獲取的就是實例的屬性,有點類似作用域局部和全局的味道
print(a.x,a.y,a.aa) # 實例找變量的時候,會先找自己的,再找類擁有的
print(A.aa) # 類變量也可以通過類來調(diào)用
# print(A.x,A.y) # 實例變量不能通過類來方法

打印結(jié)果:
2 3 100
11

5.類屬性和實例屬性以及查找順序

屬性: 變量和方法統(tǒng)稱為屬性
Python3采用的屬性查找順序是C3算法,也就是先廣度優(yōu)先,然后有子類會調(diào)用子類的方式.
通過一個類的mro屬性可以查看一個類的屬性查找順序.

# encoding:utf-8
__author__ = 'Fioman'
__time__ = '2019/3/14 19:03'
# 新式類
class D:
    pass
class E:
    pass
class C(E):
    pass
class B(D):
    pass
class A(B,C):
    pass
print(A.__mro__)

(<class'__main__.A'>, <class '__main__.B'>, <class'__main__.D'>, <class '__main__.C'>, <class '__main__.E'>, <class 'object'>)

6.類方法,靜態(tài)方法,實例方法

類方法是屬于類的,所有的對象和類公有這個類方法,用@classmethod裝飾器來裝飾.實例對象和類對象都可以調(diào)用類方法,但是類方法不能使用實例屬性,里面必須有cls參數(shù),也就是一個類參數(shù)

靜態(tài)方法,跟一個普通的函數(shù)定義沒有什么不同,對參數(shù)也沒有要求,它就是普通的函數(shù)放到一個類中,然后加上@staticmethod裝飾器.靜態(tài)方法,一般不能使用實例變量.類對象和實例對象都可以調(diào)用靜態(tài)方法

實例方法,第一個參數(shù)必須是具體的實例對象,我們一般定義的時候使用self來代替,不需要添加額外的裝飾器.實例方法一般只有實例對象才可以調(diào)用,類對象不可以調(diào)用

# encoding:utf-8
__author__ = 'Fioman'
__time__ = '2019/3/14 19:08'


class Date:
    # 構(gòu)造初始化函數(shù)
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day
    @staticmethod
    def parse_from_string(date_str):
        year,month,day = tuple(date_str.split('-'))
        return Date(int(year),int(month),int(day))

    @classmethod
    def from_string(cls,date_str):
        year,month,day = tuple(date_str.split('-'))
        return cls(int(year),int(month),int(day))

    def tomorrow(self):
        self.day += 1
        return self.day

    def __str__(self):
        return "{year}/{month}/{day}".format(year=self.year, month=self.month, day=self.day)

if __name__ == '__main__':
    new_day = Date(2019,1,2)
    print(new_day)

    print(new_day.tomorrow())

    date_str = '2018-12-31'
    # 用staticmethod來完成初始化
    new_day = Date.parse_from_string(date_str)
    print(new_day)

    # 用classmethod來完成初始化
    new_day = Date.from_string(date_str)
    print(new_day)

7.數(shù)據(jù)封裝和私有屬性.

私有屬性:
通過雙下劃線來聲明私有屬性,不可以直接訪問,但是它只是改變了一個名字不可以直接訪問.但是還是可以通過_classname__var的方式來訪問.

這種方法,其實就是為了使得我們寫程序的時候更加規(guī)范,更多的是一種提示程序員這個變量該怎么使用的作用.
這種方法還有一個好處,就是有效的區(qū)分了繼承之后變量名重名的問題,因為它會在變量前面加上自己的_classname

# encoding:utf-8
__author__ = 'Fioman'
__time__ = '2019/3/14 19:45'


class User:
    def __init__(self, birthyear):
        self.__birthyear = birthyear

    def get_age(self):
        # 返回年齡
        return 2019 - self.__birthyear


if __name__ == '__main__':
    user = User(1990)
    # print(user.__birthyear) # 這里訪問不到,以雙下劃線開頭的私有屬性
    # 不可以直接通過實例對象來訪問,但是可以通過_User__birthyear來訪問.
    print(user.get_age())

8.Python的自省

什么是自省
自省就是在Python中一個對象在運(yùn)行的時候可以通過某種機(jī)制知道自己的類型.
dir()可以獲取一個對象的多有的屬性列表,只有屬性沒有值.
__dict__變量可以獲取一個對象的自己的屬性,一般是對象自身擁有的,不是繼承的,也不是內(nèi)置的.
并且它返回的是一個字典,字典的鍵就是屬性名,字典的值就是屬性變量對應(yīng)的值.可以通過這個字典進(jìn)行添加和修改屬性的操作.

# encoding:utf-8
__author__ = 'Fioman'
__time__ = '2019/3/14 19:57'


class Person:
    name = 'user'


class Student(Person):
    def __init__(self, school_name):
        self.school_name = school_name


if __name__ == '__main__':
    user = Student('慕課網(wǎng)')
    person = Person()
    # 通過__dict__查詢這個類都有哪些屬性
    print(user.__dict__) #{'school_name': '慕課網(wǎng)'}
    print(person.__dict__) # {}
    print(Person.__dict__)
    # {'__module__': '__main__', 'name': 'user', '__dict__': <attribute '__dict__' of 'Person' objects>,
    # '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}

    # 可以通過__dict__添加和修改一個對象的屬性
    user.__dict__['school_addr'] = '北京市'
    print(user.school_addr) # 北京市

    print(dir(user))
    # dir()函數(shù)會列出一個對象的所有的屬性,包括它繼承到的,還有內(nèi)置的各種屬性

Python中的super()函數(shù)的使用

兩個問題?
1.比如我們已經(jīng)重寫了某個方法,為什么還要用super()調(diào)用它的上一級的這個方法
因為有時候,我們需要重用它的上一級完成的某些功能.比如我們重寫Thread類的時候,就可以重用它的__init__里面的一些功能

# encoding:utf-8
__author__ = 'Fioman'
__time__ = '2019/3/14 20:12'
# 既然我們重寫了構(gòu)造方法,為什么還要去調(diào)用super()呢?
# 因為我們有時候要重用它的上一級的類的方法去實現(xiàn)某種功能.

# 還有super()并不是簡單的調(diào)用父類的方法,它的調(diào)用順序又是怎么樣的呢?
# super()的調(diào)用順序是按照__mro__的順序來調(diào)用的,也就是說它是和通過.
# 來查詢屬性的順序是一致的.

from threading import Thread


class MyThread(Thread):
    def __init__(self, name, target, time):
        self.time = time
        super().__init__(target=target, name=name)

    def run(self):
        pass

class A:
    def __init__(self):
        print('A')

class B(A):
    def __init__(self):
        print('B')
        super().__init__()

class C(A):
    def __init__(self):
        print('C')
        super().__init__()

class D(B,C):
    def __init__(self):
        print('D')
        super().__init__()

if __name__ == '__main__':
    d = D()
    print(D.__mro__)

[output:] D B C A
(<class 'main.D'>, <class 'main.B'>, <class 'main.C'>, <class 'main.A'>, <class 'object'>)

如果只是簡單的調(diào)用父類super()方法,則上面的打印結(jié)果應(yīng)該是 d=D()的時候應(yīng)該是:D,B,A,C,A
但是實際的調(diào)用情況是D B C A和D.__mro__
的順序是一致的

Python中的with語句

首先看下try ... except .. else ... finally的用法
try: 先執(zhí)行的模塊,這里會捕獲異常.
except:如果捕獲到異常就會執(zhí)行except語句的代碼
else: 如果沒有捕獲到異常,就會執(zhí)行else語句的代碼
finally: 無論except和else是否執(zhí)行了,finally一定會執(zhí)行.

def exe_try():
    try:
        print('code started')
        raise KeyError
        return 1
    except KeyError as e:
        print('key error')
        return 2
    else:  # 沒有拋異常的時候會運(yùn)行
        print('other error')
        return 3
    finally:  # 無論是否拋異常都會執(zhí)行,如果finally里面有return語句.則
        # 返回的永遠(yuǎn)都是finally里面的return語句
        print('finally')
        return 4

總結(jié):finally一般用來釋放資源,做最后的處理.

Python的with語句
with語句又叫上下文管理器,之所以一個對象可以使用with語句,是因為這個對象實現(xiàn)了上下文管理器協(xié)議.
而這個協(xié)議的實現(xiàn),就是通過魔法方法__enter__()__exit__來實現(xiàn)的.
只要我們使用了with語句,它后面跟的對象,在初始化的時候就會調(diào)用這個對象的__enter__()方法,而在結(jié)束的時候就會自動調(diào)用它的__exit__()方法.

class Sample():
    def __enter__(self):
        print('__enter__')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit')

    def do_something(self):
        print('doing something')

# with表示的含義是初始化的時候先調(diào)用對象的__enter__方法
# 結(jié)束的時候會調(diào)用__exit__方法  __enter__ 和__exit__方法就是
# 用來實現(xiàn)上下文管理器協(xié)議的,然后實現(xiàn)了這個協(xié)議的就可以使用with語句.
with Sample() as simple:
    simple.do_something()

還有另外一種方式可以實現(xiàn)上下文管理器,使用contexlib的contextmanager裝飾器可以將一個函數(shù)變成上下文管理器對象.


注意:這里的函數(shù)必須是使用yield,yield之前的代碼可以理解成都是__enter__()實現(xiàn)的邏輯代碼.
而yield之后的代碼可以理解成都是__exit__()實現(xiàn)的代碼邏輯.

@contextlib.contextmanager
def file_open(file_name):
    print('file open')
    yield {}
    print('file end')

with file_open('bob.txt') as f_opened:
    print('中間')

output:
file open
中間
file end

這種書寫的好處是,代碼比較容易理解,更加的直觀.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子掠归,更是在濱河造成了極大的恐慌房揭,老刑警劉巖杨刨,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件杜跷,死亡現(xiàn)場離奇詭異便锨,居然都是意外死亡氏仗,警方通過查閱死者的電腦和手機(jī)吉捶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來皆尔,“玉大人呐舔,你說我怎么就攤上這事】度洌” “怎么了珊拼?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長流炕。 經(jīng)常有香客問我澎现,道長,這世上最難降的妖魔是什么浪感? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任昔头,我火速辦了婚禮,結(jié)果婚禮上影兽,老公的妹妹穿的比我還像新娘揭斧。我一直安慰自己,他們只是感情好峻堰,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布讹开。 她就那樣靜靜地躺著,像睡著了一般捐名。 火紅的嫁衣襯著肌膚如雪旦万。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天镶蹋,我揣著相機(jī)與錄音成艘,去河邊找鬼赏半。 笑死,一個胖子當(dāng)著我的面吹牛淆两,可吹牛的內(nèi)容都是我干的断箫。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼秋冰,長吁一口氣:“原來是場噩夢啊……” “哼仲义!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起埃撵,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎暂刘,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捂刺,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了绪商。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片苛谷。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡腹殿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出例书,到底是詐尸還是另有隱情,我是刑警寧澤决采,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站树瞭,受9級特大地震影響拇厢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜晒喷,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一孝偎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧凉敲,春花似錦衣盾、人聲如沸寺旺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽阻塑。三九已至,卻和暖如春徽龟,著一層夾襖步出監(jiān)牢的瞬間叮姑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工据悔, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留传透,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓极颓,卻偏偏與公主長得像朱盐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子菠隆,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容