主頁 > 後端開發 > 溫故而知新--day2

溫故而知新--day2

2021-01-18 06:14:24 後端開發

溫故而知新--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)的概念,在學習面向物件這個編程范式的程序中,有三個名詞我們必須了解其概念并熟悉如何運用,那就是封裝、繼承、多型,

封裝

封裝就類似于把類當作一個箱子,把資料屬性和函式屬性裝在一起,在使用的這個箱子的程序中,我們只需要根據箱子外面開的洞,把東西從外面放進去或把從里面拿出來,也就是說封裝的本質時為了區分內外,
實作封裝的方法:

  1. 雙下劃線:__
    class Dog:
    	__name = "Dog"
    
    d = Dog()
    print(d.__name)   # AttributeError: 'Dog' object has no attribute '__name'
    print(d._Dog__name)
    
    這種方法,雖然可以不然使用者直接通過欄位名訪問,但是可以通過._類名__欄位名的方式訪問,訪問起來較為復雜,
  2. 單下劃線:_
    class Dog:
    	_name = "Dog"
    
    
    d = Dog()
    print(d._name)  # Dog
    這是python程式員約定俗成的命名方式,表示此欄位不允許外部訪問,
    在實際開發中,我們并不建議將屬性設定為私有的,因為這會導致子類無法訪問,但假如直接暴露個用戶的話,也不太合適,所以建議使用裝飾器:
  • 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
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'>

反射

反射是程式可以訪問、測驗和修改其本身狀態或行為的一種能力,利用反射可以實作可插拔機制,可以提前定義好介面,只有在介面實作后真正執行,提高協同開發的效率,

  1. hasattr(obj, name)
    判斷是否可以呼叫,即可否呼叫obj.name
  2. getattr(obj, name, default=None)
    訪問某個屬性,有則回傳obj.name; 無就回傳default的內容
  3. setattr(obj, key, val)
    等同obj.key = val, 不過val引數可以使用lambda函式
  4. 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模塊

一些常用的內置裝飾器

  1. 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
    
  2. 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))
  3. 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))
    
  4. 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))
    
    結果
    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
    使用lru_cache后:
    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))
    
    
    結果
    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可以大大減少對于某些重復計算,極大地優化性能,其除了可以在遞回演算法中使用,也可以在web獲取資訊的應用中發揮作用,
    lru_cache有兩個引數可以配置lru_cache(maxsize=128, type=False),所以使用lru_cache時需要用像呼叫函式一樣使用,
    • maxsize 指定能存盤多少個呼叫的結果
    • typed 為True時,把不同型別的結果分開保存,如把1和1.0區分開來,
  5. 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中的實體屬性在實體化時會委托給描述符,所以類屬性不會被替換掉,

注意事項:
一 描述符本身應該定義成新式類,被代理的類也應該是新式類
二 必須把描述符定義成這個類的類屬性,不能為定義到建構式中
三 要嚴格遵循該優先級,優先級由高到底分別是

  1. 類屬性
  2. 資料描述符
  3. 實體屬性
  4. 非資料描述符
  5. 找不到的屬性觸發__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())

描述符用法總結

  1. 要設定只讀屬性可以使用property
  2. 用于驗證的描述符可以只有__set__
  3. 僅有__get__的描述符可以實作高效快取
  4. 沒有__set__的描述符可以被覆寫

參考資料:

  1. 《Fluent Python》
  2. 面向物件進階

我的github
我的博客
我的筆記

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/250051.html

標籤:其他

上一篇:[Vue+Flask]前后端分離JWT用戶認證授權

下一篇:shiro的簡介(一)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more