溫故而知新--day2
類
類與物件
類是一個抽象的概念,是指對現實生活中一類具有共同特征的事物的抽象,其實列化后稱為物件,類里面由類屬性組成,類屬性可以分為資料屬性和函式屬性(函式屬性又稱為類方法),舉個例子,人類是一各抽象的概念這就相當于一個類,我們的一個個的人,就是人類這個類實體化后的物件,人可以有錢這個具體的變數,這可以稱為資料屬性,而錢的多少可以是由父輩繼承過來的,也可以靠自己的奮斗組成;一些動作如吃飯,可以是為視為函式屬性或者類方法,
資料屬性
python中的類屬性就是在類中定義的變數,它可以跟隨這類為實體化后生成的物件提供資料,
class Foo:
name = "foo"
f = Foo() # 實體化
print(f.name)
不過類屬性并不是不可被改變的,它可以通過賦值的方式改變,
class Foo:
name = "foo"
f = Foo() # 實體化
print(f.name)
f.name = "f2"
print(f.name)
對于某些情況,我們的每個物件賦予不同的屬性,這就需要使用一個特殊的方法:__init__(),這個函式可以視為建構式,
class Foo:
def __init__(self, name):
self.name = name # 把name設定為物件的屬性
f = Foo("xxx") # 實體化
print(f.name)
類方法
上面說過類方法實質上就是一個個函式,不過這些函式大多要把第一的引數值設為self(這是一個約定俗成的規則,也可以用其他的名字),表示的是實體化后的物件本身,后面的引數可以和其他函式一樣設定,
class Dog:
def __init__(self, name):
self.name = name
def eat(self, food):
print(f"{self.name}吃了{food}")
d = Dog("大黃")
d.eat("狗糧")
上面的例子中,我定義可一個類,里面由name屬性和eat方法,實體化后,又呼叫了eat方法,需要注意的是,我們呼叫方法的時侯并沒有 傳入self引數,這是因為python在執行程序中會自動傳入把物件本身傳入到self中,使用classmethod裝飾器時的cls也是一樣的道理,
面向物件
面向物件編程——Object Oriented Programming,簡稱OOP,是一種程式設計思想,OOP把物件作為程式的基本單元,一個物件包含了資料和操作資料的函式,
面向程序的程式設計把計算機程式視為一系列的命令集合,即一組函式的順序執行,為了簡化程式設計,面向程序把函式繼續切分為子函式,即把大塊函式通過切割成小塊函式來降低系統的復雜度,
而面向物件的程式設計把計算機程式視為一組物件的集合,而每個物件都可以接收其他物件發過來的訊息,并處理這些訊息,計算機程式的執行就是一系列訊息在各個物件之間傳遞,
在Python中,所有資料型別都可以視為物件,當然也可以自定義物件,自定義的物件資料型別就是面向物件中的類(Class)的概念,在學習面向物件這個編程范式的程序中,有三個名詞我們必須了解其概念并熟悉如何運用,那就是封裝、繼承、多型,
封裝
封裝就類似于把類當作一個箱子,把資料屬性和函式屬性裝在一起,在使用的這個箱子的程序中,我們只需要根據箱子外面開的洞,把東西從外面放進去或把從里面拿出來,也就是說封裝的本質時為了區分內外,
實作封裝的方法:
- 雙下劃線:
__:
這種方法,雖然可以不然使用者直接通過欄位名訪問,但是可以通過class Dog: __name = "Dog" d = Dog() print(d.__name) # AttributeError: 'Dog' object has no attribute '__name' print(d._Dog__name)._類名__欄位名的方式訪問,訪問起來較為復雜, - 單下劃線:
_
這是python程式員約定俗成的命名方式,表示此欄位不允許外部訪問,class Dog: _name = "Dog" d = Dog() print(d._name) # Dog
在實際開發中,我們并不建議將屬性設定為私有的,因為這會導致子類無法訪問,但假如直接暴露個用戶的話,也不太合適,所以建議使用裝飾器:
- property
- setter
class Dog:
_name = "Dog"
_age = 5
@property
def name(self):
return self._name
@property
def age(self):
return self._age
@age.setter
def age(self, value):
self._age = value
d = Dog()
print(d.name) # Dog
# d.name = 123 # AttributeError: can't set attribute
print(d.age) # 5
d.age += 1
print(d.age) # 6
繼承
顧名思義,繼承是指在使用自己沒有的屬性時,可以尋找父類的屬性,找得到就用,找不到就報錯;舉個例子,繼承有點像現實中的繼承財產,假如你沒有錢可以使用從父輩那里繼承過來的財產使用,但不同的是:假如你已經掙了錢,那么就不能直接從父輩中拿錢過來用了,因此,繼承是需要父子關系的,其中一個父類可以有多個子類,而一個子類也可以有多個父類,
class Person:
"""人"""
def __init__(self, name, age):
self._name = name
self._age = age
@property
def name(self):
return self._name
@property
def age(self):
return self._age
class Student:
"""學生"""
def __init__(self, name, age, grade):
super().__init__(name, age)
self._grade = grade
@property
def grade(self):
return self._grade
@grade.setter
def grade(self, grade):
self._grade = grade
def study(self, course):
print(f"{self.name}正在學習{course}")
if __name__ == '__main__':
stu = Student('王二狗', 18, '高三')
print(stu.name, stu.age, stu.grade)
stu.study('數學')
例子中的super()是子類呼叫父類的方法,其格式是:super().方法(引數),
關于繼承的順序
假如一個類有多個父類,那么它是如何選擇從哪個父類開始找的呢?
# 假如逐一注釋name欄位的話,就會出現注釋后面的內容
class A:
name = 'A' # AttributeError: 'D' object has no attribute 'name'
pass
class B(A):
name = 'B' # C
pass
class C(A):
name = 'C' # A
pass
class D(B, C):
pass
d = D()
print(d.name)
上述例子,B和C繼承A,D繼承B和C,在Python中的繼承順序有兩種:
- 深度優先
- 廣度優先

python2 經典類是按深度優先來繼承的,新式類是按廣度優先來繼承的,
python3 統一按廣度優先來繼承的,
另外python3不再區分經典類和新式類,即寫的類不繼承object也是新式類,
多型
子類在繼承了父類的方法后,可以對父類已有的方法給出新的實作版本,這個動作稱之為方法重寫,通過方法重寫我們可以讓父類的同一個行為在子類中擁有不同的實作版本,當我們呼叫這個經過子類重寫的方法時,不同的子類物件會表現出不同的行為,這個就是多型,比如貓和狗都屬于生物,它們都有叫這個方法,但是他們叫之后的效果是不一樣的,這種現象就類似于多型,
class Animal:
def __init__(self, name):
self.name = name
def jiao(self):
pass
class Cat(Animal):
def __init__(self, name):
super().__init__(name)
def jiao(self):
print("%s: 喵喵喵~" % self.name)
class Dog(Animal):
def __init__(self, name):
super().__init__(name)
def jiao(self):
print("%s: 汪汪汪~" % self.name)
c = Cat("小貓")
d = Dog("大黃")
c.jiao()
d.jiao()
類常用的雙下劃線方法
如果方法名前如果有兩個下劃線,則表示該成員是私有成員,私有成員只能由類內部呼叫,python的類中就有這些雙下劃線方法,我們在定義自己的類的同時,可以對其進行重寫,以達到自己想要的效果,
__doc__和__name__
表示類的描述資訊
class Foo:
"""this is the Foo class"""
pass
print(Foo.__name__) # Foo
print(Foo.__doc__) # this is the Foo class
__module__和__class__
- module 表示當前操作的物件在那個模塊
- class 表示當前操作的物件的類是什么
from collections import Counter
print(Counter.__module__) # collections
print(Counter.__class__) # <class 'type'>
class Foo:
pass
f = Foo()
print(f.__module__) # __main__ # 注意假如是匯入的話,__main__就會變為模塊名,下面一樣
print(f.__class__) # <class '__main__.Foo'>
__init__
建構式,實體化時自動執行,接收物件,禁止回傳任何值,更多見__new__,
class Foo:
def __init__(self):
print("running __init__")
f = Foo() # running __init__
__del__
析構方法,當物件在記憶體中被釋放時,自動觸發執行,由解釋器進性垃圾回收時自動觸發,無需我們自定義,
典型的應用場景:
創建資料庫類,用該類實體化出資料庫鏈接物件,物件本身是存放于用戶空間記憶體中,而鏈接則是由作業系統管理的,存放于內核空間記憶體中
當程式結束時,python只會回收自己的記憶體空間,即用戶態記憶體,而作業系統的資源則沒有被回收,這就需要我們定制__del__,在物件被洗掉前向作業系統發起關閉資料庫鏈接的系統呼叫,回收資源
__call__
為物件加括號時觸發,
class Foo:
def __call__(self, *args, **kwargs):
print("__call__")
f = Foo()
f() # 呼叫__call__
__dict__、__slots__
- dict 以字典的形式存盤類或物件的所有成員
- slots 在元組中存盤物件的屬性,可以大大減少記憶體,該屬性不可被繼承
class Foo:
name = 'foo'
def __init__(self, arg):
self.arg = arg
def run(self):
pass
print(Foo.__dict__)
# {'__module__': '__main__', 'name': 'foo', '__init__': <function Foo.__init__ at 0x0000025C3A1FD310>,
# 'run': <function Foo.run at 0x0000025C3A1FD3A0>, '__dict__': <attribute '__dict__' of 'Foo' objects>,
# '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None}
print(Foo("test").__dict__) # {'arg': 'test'}
class Bar:
__slots__ = ("name", "value")
def __init__(self, name, value):
self.name = name
self.value = https://www.cnblogs.com/lczmx/archive/2021/01/17/value
def run(self):
pass
b = Bar("lczmx", "test")
print(Bar.__slots__) # 'name', 'value')
print(b.__slots__) # ('name', 'value')
# b.age = 20 # AttributeError: 'Bar' object has no attribute 'age
雖然__slots__可以限制程式員新增物件屬性,但是可以通過在__slots__中添加__dict__解決,可我們使用__slots__的主要目的是替換以散串列形式存盤資料的字典,降低所需的記憶體,所以如何使用還看個人的選擇,另外假如要把實體作為弱參考的目標,可以加入__weakref__屬性,
__str__、__repr__
- str 將python中的物件轉換為字串, 主要面向用戶,其目的是可讀性
- repr 將python中的物件轉換為字串, 面向的是python解釋器,或者說開發人員,其目的是準確性
使用順序:
- print 函式呼叫:
- 沒有說明用str或repr時,優先__str__
- 指定為str沒有__str__時,使用__repr__
- 指定為repr沒有__repr__時,回傳物件記憶體地址
- 命令列終端呼叫:
- 有__repr__回傳__repr__,無則回傳物件記憶體地址
class Foo:
def __str__(self):
return "str foo"
def __repr__(self):
return "repr foo"
class Bar:
def __repr__(self):
return "repr bar"
class Test:
def __str__(self):
return "str test"
f = Foo()
b = Bar()
t = Test()
print(str(f)) # str foo
print(repr(f)) # repr foo
print(f) # str foo
print(b) # repr bar
print(str(b)) # repr bar
print(repr(t)) # <__main__.Test object at 0x0000020AD6859B80>
使用命令列運行,python -i cls.py (cls.py是代碼所在檔案)
>>> b
repr bar
>>> f
repr foo
>>> t
<__main__.Test object at 0x000001FA9F3E9B80>
>>>
__format__
可以自定義格式化,使用format()方法呼叫,
class Foo:
def __init__(self, name, age):
self.name = name
self.age = age
def __format__(self, value: str):
fmt_dict = {
"n-a": "{name}-{age}",
"n(a)": "{name}({age})",
"N:A": "name: {name}, age: {age}",
}
fmt_string = fmt_dict.get("n-a")
if fmt_dict.get(value):
fmt_string = fmt_dict.get(value)
res_fmt = fmt_string.format(name=self.name, age=self.age)
return res_fmt
f = Foo("lczmx", 20)
print(format(f)) # lczmx-20
print(format(f, "n(a)")) # lczmx(20)
print(format(f, "N:A")) # name: lczmx, age: 20
__getattr__和__getattribute__、__setattr__、__delattr__
__getattr__訪問屬性(方法或欄位)不存在時觸發,即拋出AttributeError時觸發__getattribute__訪問屬性時,不管存不存在都觸發,不存在時拋出AttributeError,所以其先于__getattr__,__setattr__設定屬性(增/改)時觸發(可以放入__dict__)__delattr__洗掉屬性時觸發
注意: 重寫這幾個方法(包括下面的__xxxitem__方法),特別容易造成無限遞回,注意這里操作的是__dict__,而不是通過.的方式,
例子,注意如何操作__dict__,以及這幾種方法什么時候執行,
class Foo:
def __init__(self, name):
self.name = name
def __getattr__(self, key):
print("getattr: 知道%s不存在了" % key)
def __getattribute__(self, item):
print("getattribute:訪問了%s" % item)
return super().__getattribute__(item)
def __setattr__(self, key, value):
print("setattr: 設定了%s=%s" % (key, value))
# self.key=value # 無限遞回
self.__dict__[key] = value
def __delattr__(self, key):
if key in self.__dict__:
print("delattr: 洗掉了%s" % key)
# del self.key # 無限遞回
self.__dict__.pop(key, None) # 或 del self.__dict__[key]
else:
print("delattr: %s不存在" % key)
f = Foo("lczmx")
# setattr: 設定了name=lczmx
# getattribute:訪問了__dict__
f.name = "foo"
# setattr: 設定了name=foo
# getattribute:訪問了__dict__
print(f.name)
# getattribute:訪問了name
# foo
del f.name
getattribute:訪問了__dict__
delattr: 洗掉了name
getattribute:訪問了__dict__
f.name
# getattribute:訪問了name
# getattr: 知道name不存在了
__getitem__、__setitem__、__delitem__
__getitem__用于索引操作:獲取__setitem__用于索引操作:設定__delitem__用于索引操作:洗掉
注:索引操作是指用中括號操作,用.操作呼叫的是__xxxattr__,這里操作的也是__dict__
class Foo:
def __init__(self, name):
self.name = name
def __getitem__(self, key):
print("getitem: 獲取%s" % key)
return self.__dict__[key]
def __setitem__(self, key, value):
print("setitem: 設定了%s=%s" % (key, value))
self.__dict__[key] = value
def __delitem__(self, key):
if key in self.__dict__:
print("delitem: 洗掉了%s" % key)
self.__dict__.pop(key, None) # 或 del self.__dict__[key]
else:
print("delitem: %s不存在" % key)
f = Foo('foo')
f['age'] = 18 # setitem: 設定了age=18
f['age1'] = 19 # setitem: 設定了age1=19
del f['age1'] # delitem: 洗掉了age1
f['name'] = 'lczmx' # setitem: 設定了name=lczmx
print(f.__dict__) # {'name': 'lczmx', 'age': 18}
__iter__、__next__:迭代器協議
迭代器協議:物件必須提供一個next方法,執行該方法要么回傳下一項,要么拋出StopIteration例外,終止迭代器,
class Foo:
def __init__(self, stop, start=0, seq=1):
self.start = start
self.stop = stop
self.seq = 1 if int(seq) == 0 else int(abs(seq)) # 確保seq是大于0的整數
self._reverse = False # stop到start
if start > stop:
self._reverse = True
def __next__(self):
if (self.start < self.stop and not self._reverse) or \
(self.start > self.stop and self._reverse):
if self._reverse: # stop -> start
n = self.stop
self.stop += self.seq
else: # start -> stop
n = self.start
self.start += self.seq
return n
raise StopIteration
在命令列中運行:
>>> f = Foo(start=1, stop=6, seq=2)
>>> next(f)
1
>>> next(f)
3
>>> next(f)
5
>>> next(f)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "cls.py", line 21, in __next__
raise StopIteration
StopIteration
>>>
for回圈可以呼叫__iter__方法,將物件變為可迭代物件,同時for回圈內部遇到StopIteration例外時,停止迭代,
為了支持for回圈,我們可以為上面的類增加一個__iter__方法:
class Foo:
def __init__(self, stop, start=0, seq=1):
self.start = start
self.stop = stop
self.seq = 1 if int(seq) == 0 else int(abs(seq)) # 確保seq是大于0的整數
self._reverse = False # stop到start
if start > stop:
self._reverse = True
def __next__(self):
if (self.start < self.stop and not self._reverse) or \
(self.start > self.stop and self._reverse):
if self._reverse: # stop -> start
n = self.stop
self.stop += self.seq
else: # start -> stop
n = self.start
self.start += self.seq
return n
raise StopIteration
def __iter__(self):
return self
關于iter函式:iter函式可以呼叫__iter__方法,生成可迭代物件,且它有兩個引數iter(object[, sentinel]):
- 第一個引數是遵循可迭代協議或支持序列協議(有
__getitem__()方法,且數字引數從 0 開始)的物件; - 第二個引數有值的話,object 必須是可呼叫的物件,這種情況下生成的迭代器,每次迭代呼叫它的
__next__()方法時都會不帶實參地呼叫 object;如果回傳的結果是sentinel則觸發StopIteration,否則回傳呼叫結果(這種效果可以作為哨兵使用),
class Foo:
def __init__(self, stop, start=0, seq=1):
self.start = start
self.stop = stop
self.seq = 1 if int(seq) == 0 else int(abs(seq)) # 確保seq是大于0的整數
self._reverse = False # stop到start
if start > stop:
self._reverse = True
def __next__(self):
if (self.start < self.stop and not self._reverse) or \
(self.start > self.stop and self._reverse):
if self._reverse: # stop -> start
n = self.stop
self.stop += self.seq
else: # start -> stop
n = self.start
self.start += self.seq
return n
raise StopIteration
def __call__(self):
return next(self)
def __iter__(self):
return self
if __name__ == '__main__':
f = Foo(start=1, stop=10)
i = iter(f, 3)
print(next(i)) # 1
print(next(i)) # 2
print(next(i))
"""
Traceback (most recent call last):
File "cls.py", line 35, in <module>
print(next(i))
StopIteration
"""
__enter__和__exit__:背景關系管理協議
背景關系管理協議,即with陳述句,為了讓一個物件兼容with陳述句,必須在這個物件的類中宣告__enter__和__exit__方法,with陳述句開始時,背景關系管理物件會呼叫__enter__方法,with陳述句運行結束后,呼叫__exit__方法,
優點是可以自動釋放資源,在一些需要管理資源的場景(檔案、網路連接、鎖等)大有用處,
class Test:
def __init__(self, data):
self.data = https://www.cnblogs.com/lczmx/archive/2021/01/17/data
def __enter__(self):
print(self.data)
return self # 要使用as 的話,注意回傳self
def __exit__(self, exc_type, exc_value, exc_tb):
print(exc_type, exc_value, exc_tb)
# exc_type 例外名
# exc_value 例外值
# exc_tb 例外追蹤資訊
# 三個都沒值時,為None
def test_func(self, args):
print(args)
if __name__ == '__main__':
with Test("123") as t:
t.test_func("test")
使用標準庫實作背景關系管理器:
@contextlib,contextmanager+yield:在被contextmanager裝飾的生成器中,yield前的部分相當于__enter__,之后的部分相當于__exit__,例子:
from contextlib import contextmanager
def acquire_resource(*args, **kwargs):
"""處理資料的函式"""
return "已經處理過的資料"
@contextmanager
def test(*args, **kwargs):
try: # 必須做例外處理
# 處理資料的代碼
data = https://www.cnblogs.com/lczmx/archive/2021/01/17/acquire_resource(*args, **kwargs)
yield data
finally:
# 釋放資源的代碼
# Code to release resource, e.g.:
print("release resource")
if __name__ == "__main__":
with test(12) as t:
print(t) # 已經處理過的資料
# # release resource
__new__、__metaclass__
**python一切皆物件,**所有的類也都是物件,那么類是有誰產生的呢?實際上python中的類是由type類實體化產生的,而為了避免無限溯源,type類又是其本身的實體化,為了更好的理解,可以看看下面這幅圖:

type與object
兩幅圖都是正確的,左邊強調str、type和LineItem是object的子類,右邊強調str、object、LineItem是type的實體,
object和type的關系很特別:object是type的實體,type是object的子類,
回歸到__new__和__metaclass__上,前面在__init__里說過:__init__稱為建構式,實質上用于構造實體的方法是__new__,__new__是一個經過特殊處理的類方法,不必使用classmethod,它必須回傳一個實體,而這個實體會作為__init__的第一個引數(self),所以__init__實質上就是“初始化方法”,一般來說我們不需要重寫__new__方法,繼承object的就已經夠用了,
__metaclass__是用來表示該類由誰來實體化創建的,
class Bar:
def __init__(self):
super().__init__()
def __new__(cls, *args, **kwargs):
print("bar new running")
return object.__new__(cls, *args, **kwargs)
class Foo:
__metaclass__ = Bar
def __new__(cls, *args, **kwargs):
print("foo new running")
return object.__new__(cls, *args, **kwargs)
b = Bar() # bar new running
f = Foo() # foo new running
print(f.__metaclass__) # <class '__main__.Bar'>
反射
反射是程式可以訪問、測驗和修改其本身狀態或行為的一種能力,利用反射可以實作可插拔機制,可以提前定義好介面,只有在介面實作后真正執行,提高協同開發的效率,
- hasattr(obj, name)
判斷是否可以呼叫,即可否呼叫obj.name - getattr(obj, name, default=None)
訪問某個屬性,有則回傳obj.name; 無就回傳default的內容 - setattr(obj, key, val)
等同obj.key = val, 不過val引數可以使用lambda函式 - delattr(obj, key)
等同于del obj.key
例子:
class HomeWork:
def __init__(self, course, complete=False):
self.course = course
def write(self, content):
self.content = content
self.wrong = "寫錯了"
def __str__(self):
"""方便顯示"""
show_str = ""
for k, v in self.__dict__.items():
show_str += "%s = %s\n" % (k, v)
return show_str
if __name__ == '__main__':
h = HomeWork("語文")
# hasattr
if hasattr(h, "write"):
# getattr
func = getattr(h, "write")
func("鵝鵝鵝,曲項向天歌!")
# setattr
setattr(h, "complate", True)
# delattr
delattr(h, "wrong")
print(h)
# course = 語文
# content = 鵝鵝鵝,曲項向天歌!
# complate = True
裝飾器
在我們日常開發的程序中,往往不能做到面面俱到或由于當時業務不需要,所以后期需要用到某些功能時可能要修改某些代碼,但是直接寫個原函式可能造成不可挽回的損失,為了解決這種情況,我們可以使用裝飾器來維護代碼,除此之外,裝飾器經常用于有切面需求的場景,比如:插入日志、性能測驗、事務處理、快取、權限校驗等場景,它 是解決這類問題的絕佳設計,裝飾器允許向一個現有的物件添加新的功能,同時又不改變其結構,裝飾器本質上就是一個函式,其相對于高階函式+函式嵌套+閉包的組合體,三者合一成為裝飾器,
一些基本概念
想要使用裝飾器首先要了解一些基本的概念,
- 語法糖@
在python中@dec def test(): pass
相對于:test = dec(test)
- 裝飾器會在被裝飾函式定義完之后立即執行
def dec(func): print("dec running") # dec running @dec def test(): pass
可以看到,我并沒有呼叫test函式,dec就執行了,所以在匯入模塊時需要注意裝飾器是否允許被執行,
- 閉包
閉包是延伸了作用域的函式,可以訪問定義體之外定義的非全域變數,其主要的形式就是用一個函式嵌套另一個函式,以達到作用域延伸的效果,def make_avg(): """計算平均數""" num_list = [] def avg(new_value): num_list.append(new_value) return sum(num_list)/len(num_list) return avg a = make_avg() print(a(10)) # 10 print(a(20)) # 15 print(a(30)) # 20

閉包示意圖
綜上,閉包是一種函式,它會保留定義函式時存在的自由變數的系結,在呼叫函式時,雖定義作用域不可用,但仍然可以使用這些系結,
簡單列子
前面說過,裝飾器實質上就是高階函式+函式嵌套+閉包的組合體,所以其基本的格式如下:
def timmer(func):
def wrapper(*args, **kwargs):
# 代碼 # 此時可以修改傳入的引數*args和, **kwargs
res = func(*args, **kwargs) # 運行原函式
# 代碼 # 可以修改原函式的回傳值res
return res # 假如有回傳的話,別忘記回傳了
return wrapper
為了更加便于理解,下面舉一個例子,用一個裝飾器計算函式的運行時間:
from time import time
def clock(func):
def clocked(*args, **kwargs):
start = time()
res = func(*args, **kwargs)
end = time()
func_name = func.__name__
print('{0}({1}) 執行了[{2:.8f}s]'.format(
func_name, ",".join(repr(i) for i in args), end-start))
return res
return clocked
@clock
def factorial(num):
"""計算階乘"""
return 1 if num < 2 else num * factorial(num - 1)
if __name__ == '__main__':
n = 6
result = factorial(n)
print("%d! = %d" % (n, result))
需要注意的是,在日常開發程序中,我們應該把裝飾器放在其他的檔案中,在使用時可以匯入到本模塊中使用,
使用標準庫實作
上面計算函式運行時間這個例子有一個缺點:原函式的__name__和__doc__屬性被覆寫了,為了解決這個問題,我們可以引入functools.wraps把原函式相關屬性復制到裝飾器中,
from functools import wraps
from time import time
def clock(func):
"""計算函式運行時間"""
@wraps(func)
def clocked(*args, **kwargs):
start = time()
res = func(*args, **kwargs)
end = time()
func_name = func.__name__
print('{0}({1}) 執行了[{2:.8f}s]'.format(
func_name, ",".join(repr(i) for i in args), end-start))
return res
return clocked
@clock
def factorial(num):
"""計算階乘"""
return 1 if num < 2 else num * factorial(num - 1)
if __name__ == '__main__':
n = 6
result = factorial(n)
print(factorial.__name__) # factorial # 之前:clocked
print(factorial.__doc__) # 計算階乘 # 之前:None
print("%d! = %d" % (n, result))
如果想查看更多關于functools庫的內容,可以查看官方檔案functools模塊
一些常用的內置裝飾器
- property
靜態屬性,可以使方法在呼叫時不需要加括號就可以直接使用,被裝飾后不能被賦值,除非使用xx.setter再裝飾一遍這個同名方法,class Foo: def __init__(self, name, age): self._name = name self._age = age @property def name(self): return self._name @property def age(self): return self._age @age.setter def age(self, value): self._age = value f = Foo("lczmx", 18) print(f.name) # f.name = "xxx" # AttributeError: can't set attribut f.age = 20 - classmethod
類方法,可以直接通過類名.方法名()呼叫,不需要實體化,也不需要創cls引數,class Foo: @classmethod def get_name(cls, data: dict): return data.get('name') data = https://www.cnblogs.com/lczmx/archive/2021/01/17/{'name': 'test'} print(Foo.get_name(data)) - staticmethod
靜態方法,類似于普通的函式,第一個引數并不是self或cls,呼叫時可以由類呼叫,也可以由物件呼叫,class Foo: @staticmethod def get_name(data: dict): return data.get('name') data = https://www.cnblogs.com/lczmx/archive/2021/01/17/{'name': 'test'} print(Foo.get_name(data)) f = Foo() print(f.get_name(data)) - functools.lru_cache
一個為函式提供快取功能的裝飾器,快取 maxsize 組傳入引數,在下次以相同引數呼叫時直接回傳上一次的結果,用以節約高開銷或I/O函式的呼叫時間,
結果from functools import wraps from time import time def clock(func): """計算函式運行時間""" @wraps(func) def clocked(*args, **kwargs): start = time() res = func(*args, **kwargs) end = time() func_name = func.__name__ print('{0}({1}) 執行了[{2:.8f}s]'.format( func_name, ",".join(repr(i) for i in args), end-start)) return res return clocked @clock def fib(num): """計算斐波那契數列""" return num if num < 2 else (fib(num - 2) + fib(num - 1)) print(fib(5))
使用lru_cache后:fib(1) 執行了[0.00000000s] fib(0) 執行了[0.00000000s] fib(1) 執行了[0.00000000s] fib(2) 執行了[0.00099683s] fib(3) 執行了[0.00299120s] fib(0) 執行了[0.00000000s] fib(1) 執行了[0.00000000s] fib(2) 執行了[0.00101113s] fib(1) 執行了[0.00000000s] fib(0) 執行了[0.00000000s] fib(1) 執行了[0.00000000s] fib(2) 執行了[0.02294302s] fib(3) 執行了[0.02392578s] fib(4) 執行了[0.02593160s] fib(5) 執行了[0.02892280s] 5
結果from functools import wraps, lru_cache from time import time def clock(func): """計算函式運行時間""" @wraps(func) def clocked(*args, **kwargs): start = time() res = func(*args, **kwargs) end = time() func_name = func.__name__ print('{0}({1}) 執行了[{2:.8f}s]'.format( func_name, ",".join(repr(i) for i in args), end-start)) return res return clocked @lru_cache() @clock def fib(num): """計算斐波那契數列""" return num if num < 2 else (fib(num - 2) + fib(num - 1)) print(fib(5))
可以發現,使用可lru_cache可以大大減少對于某些重復計算,極大地優化性能,其除了可以在遞回演算法中使用,也可以在web獲取資訊的應用中發揮作用,fib(1) 執行了[0.00000000s] fib(0) 執行了[0.00000000s] fib(2) 執行了[0.00000000s] fib(3) 執行了[0.00000000s] fib(4) 執行了[0.00000000s] fib(5) 執行了[0.00098634s] 5
lru_cache有兩個引數可以配置lru_cache(maxsize=128, type=False),所以使用lru_cache時需要用像呼叫函式一樣使用,- maxsize 指定能存盤多少個呼叫的結果
- typed 為True時,把不同型別的結果分開保存,如把1和1.0區分開來,
- functools.singledispatch
由于python沒有函式多載的概念,所以假如有 用一個函式處理不同的事情的情況就很難處理,最簡單的方法是使用大量的if-elif-else進性判斷,但是,這樣做會造成函式的越寫越臃腫,而且耦合度高,為此經過深思熟慮python3.4最終把singledispatch加入了標準庫,使解決這類問題可以使用模塊化的方法處理,
使用@singledispatch可以根據第一個引數的型別,以不同的方式處理資料,使原本的函式變為泛函式,from functools import singledispatch from numbers import Integral from collections import abc @singledispatch def print_typed(data): """處理object型別的基函式""" print("object (%s):" % type(data), data) @print_typed.register(str) def _(s): print("字串:%s" % s) @print_typed.register(Integral) # Integral是int的虛擬超類 def _(num): print("數字: %d" % num) # 可以放多個 @print_typed.register(tuple) @print_typed.register(abc.MutableSequence) # list等 def _(seq): print("seq: ", seq) if __name__ == '__main__': print_typed("123") # 字串:123 print_typed([1, 2, 3]) # seq: [1, 2, 3] print_typed({"name": "lczmx"}) # object (<class 'dict'>): {'name': 'lczmx'}
引數化裝飾器
從functools.lru_cache可以看出,裝飾器是可以加引數的,其具體的本質就是一個函式包裹著一個裝飾器,如
@dec(ars=123)
def test()
等同于test = dec(arg=123)(test),下面舉個例子,優化之前計算函式運行時間的裝飾器,為其加上一個引數,可以自定義格式化輸出:
from time import time
def clock(fmt='{func_name}({args_str}) 執行了[{elapsed}]s'):
def decorated(func):
def clocked(*args, **kwargs):
start = time()
res = func(*args, **kwargs)
end = time()
func_name = func.__name__
args_str = ",".join(repr(i) for i in args)
elapsed = end - start # 所用時間
print(fmt.format(**locals())) # 參考clocked中的全部區域變數
return res
return clocked
return decorated
@clock()
def fib(num):
"""計算斐波那契數列"""
return num if num < 2 else (fib(num - 2) + fib(num - 1))
@clock(fmt='{args_str:10} {elapsed:.8f}s ')
def factorial(num):
"""計算階乘"""
return 1 if num < 2 else num * factorial(num - 1)
print("fib(2) =", fib(2))
print("factorial(5) =", factorial(5))
結果:
fib(0) 執行了[0.0]s
fib(1) 執行了[0.0]s
fib(2) 執行了[0.0049860477447509766]s
fib(2) = 1
1 0.00000000s
2 0.00000000s
3 0.00000000s
4 0.00099707s
5 0.00199723s
factorial(5) = 120
描述符
描述符是對多個屬性運用相同存取邏輯的一種方式,是實作了特定協議的類,這個協議包括:__get__、__set__、__delete__,property就實作了這三個,在實際開發程序中,一般只需要實作部分即可,
根據實作的方法可以把描述符分為資料描述符和非資料描述符:
- 資料描述符:至少實作
__set__ - 非資料描述符:沒有實作
__set__
描述符的用法是:創建一個實體,作為另一個類的屬性,
下面展示__get__和__set__的一般用法,以及對應的引數設定和怎么在其他的類中使用,
class Viladate:
def __init__(self, storage_name):
self. storage_name = storage_name
def __get__(self, instance, owner):
print("get: instances: %s, owner: %s" % (instance, owner))
# get: instances: <__main__.Item object at 0x00000146F4F354C0>, owner: <class '__main__.Item'>
if instance is None:
return self
return instance.__dict__.get(self.storage_name)
def __set__(self, instance, value):
print("set instance: %s, value: %s" % (instance, value))
# set instance: <__main__.Item object at 0x0000022095E054C0>, value: 3
if value <= 0:
raise ValueError("value必須大于0")
instance.__dict__[self.storage_name] = value
class Item:
price = Viladate("price")
weight = Viladate("weight")
def __init__(self, price, weight):
self.price = price
self.weight = weight
def total(self):
return self.price * self.weight
if __name__ == '__main__':
i = Item(2.99, 3)
print(i.price)
i.weight = 50
print(i.total())
例子中有幾個要點:1. 操作的是托管類(Item)的物件的__dict__;2. 托管類中兩次定義屬性,init中的實體屬性在實體化時會委托給描述符,所以類屬性不會被替換掉,
注意事項:
一 描述符本身應該定義成新式類,被代理的類也應該是新式類
二 必須把描述符定義成這個類的類屬性,不能為定義到建構式中
三 要嚴格遵循該優先級,優先級由高到底分別是
- 類屬性
- 資料描述符
- 實體屬性
- 非資料描述符
- 找不到的屬性觸發__getattr__()
描述符與類裝飾器結合
上面這個例子可以進一步改進,把price = Viladate("price")換為price = Viladate(),即動態生成欄位名,要實作這個功能,需要用到類裝飾器,它和函式裝飾器差不多,差別就是把引數的func換成類,回傳值是原來的類或者新類,
為了更加貼近真實場景,這次分別用不同的檔案定義
./model.py:
# model.py
class Viladate:
storage_name = None # 作為在托管物件的__dict__中的key
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.storage_name) # 可以用getattr()
def __set__(self, instance, value):
if value <= 0:
raise ValueError("value必須大于0")
instance.__dict__[self.storage_name] = value # 可以用setattr()
def entity(cls):
"""類裝飾器"""
for k, v in cls.__dict__.items():
if isinstance(v, Viladate):
class_name = type(v).__name__
v.storage_name = f"_{class_name}#{k}"
# 格式形如: _Viladata#price
# 加'#'是防止在操作 被托管類物件時被替換
# '#'是可以被__dict__或setattr()/getattr()識別的
return cls
./main.py:
# main.py
import model
@model.entity
class Item:
price = model.Viladate() # 像Django的ORM了吧
weight = model.Viladate()
def __init__(self, price, weight):
self.price = price
self.weight = weight
def total(self):
return self.price * self.weight
if __name__ == '__main__':
i = Item(2.99, 3)
print(i.price)
i.weight = 50
# i.price = -100 # 會報錯
print(i.total())
描述符用法總結:
- 要設定只讀屬性可以使用
property - 用于驗證的描述符可以只有
__set__ - 僅有
__get__的描述符可以實作高效快取 - 沒有
__set__的描述符可以被覆寫
參考資料:
- 《Fluent Python》
- 面向物件進階
我的github
我的博客
我的筆記
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/250051.html
標籤:其他
下一篇:shiro的簡介(一)
