搜尋
首頁後端開發Python教學一起深入 Python 類別的內部

一起深入 Python 類別的內部

Nov 18, 2020 pm 05:16 PM
python類別

python影片教學欄位介紹認識Python類別的內部。

一起深入 Python 類別的內部

這篇文章和大家一起聊聊Python 3.8 中類別和物件背後的一些概念和實作原理,主要嘗試解釋Python 類別和物件屬性的存儲,函數和方法,描述器,物件記憶體佔用的最佳化支持,以及繼承與屬性查找等相關問題。

讓我們從一個簡單的範例開始:

class Employee:

    outsource = False

    def __init__(self, department, name):
        self.department = department
        self.name = name    @property
    def inservice(self):
        return self.department is not None

    def __repr__(self):
        return f"<employee:>"employee = Employee('IT', 'bobo')复制代码</employee:>

employee 物件是Employee 類別的一個實例,它有兩個屬性departmentname,其值屬於該實例。 outsource 是類別屬性,擁有者是類,該類別的所有實例物件共用此屬性值,這跟其他物件導向語言一致。

更改類別變數會影響到該類別的所有實例物件:

>>> e1 = Employee('IT', 'bobo')>>> e2 = Employee('HR', 'cici')>>> e1.outsource, e2.outsource
(False, False)>>> Employee.outsource = True>>> e1.outsource, e2.outsource>>> (True, True)复制代码

這僅限於從類別更改,當我們從實例更改類別變數時:

>>> e1 = Employee('IT', 'bobo')>>> e2 = Employee('HR', 'cici')>>> e1.outsource, e2.outsource
(False, False)>>> e1.outsource = True>>> e1.outsource, e2.outsource
(True, False)复制代码

是的,當你試圖從實例物件修改類別變數時,Python 不會更改該類別的類別變數值,而是建立一個同名的實例屬性,這是非常正確且安全的。在搜尋屬性值時,實例變數會優先於類別變量,這將在繼承與屬性尋找一節中詳細解釋。

值得特別注意的是,當類別變數的類型是可變類型時,你是從實例物件中更改的它們的:

>>> class S:...     L = [1, 2]
...>>> s1, s2 = S(), S()>>> s1.L, s2.L
([1, 2], [1, 2])>>> t1.L.append(3)>>> t1.L, s2.L
([1, 2, 3], [1, 2, 3])复制代码

好的實踐方式是應盡量的避免這樣的設計。

屬性的儲存

本小節我們一起來看看 Python 中的類別屬性、方法及實例屬性是如何關聯儲存的。

實例屬性

在Python 中,所有實例屬性都儲存在__dict__ 字典中,這就是一個常規的dict,對於實例屬性的維護即是從該字典中取得和修改,它對開發者是完全開放的。

>>> e = Employee('IT', 'bobo')>>> e.__dict__
{'department': 'IT', 'name': 'bobo'}>>> type(e.__dict__)dict>>> e.name is e.__dict__['name']True>>> e.__dict__['department'] = 'HR'>>> e.department'HR'复制代码

正因為實例屬性是採用字典來存儲,所以任何時候我們都可以方便的給對象添加或刪除字段:

>>> e.age = 30 # 并没有定义 age 属性>>> e.age30>>> e.__dict__
{'department': 'IT', 'name': 'bobo', 'age': 30}>>> del e.age>>> e.__dict__
{'department': 'IT', 'name': 'd'}复制代码

我們也可以從字典中實例化一個對象,或透過保存實例的__dict__ 來恢復實例。

>>> def new_employee_from(d):...     instance = object.__new__(Employee)...     instance.__dict__.update(d)...     return instance
...>>> e1 = new_employee_from({'department': 'IT', 'name': 'bobo'})>>> e1
<employee:>>>> state = e1.__dict__.copy()>>> del e1>>> e2 = new_employee_from(state)>>> e2>>> <employee:>复制代码</employee:></employee:>

因為__dict__ 的完全開放,所以我們可以在其中加上任何hashableimmutable key,例如數字:

>>> e.__dict__[1] = 1>>> e.__dict__
{'department': 'IT', 'name': 'bobo', 1: 1}复制代码

這些非字串的欄位是我們無法透過實例物件存取的,為了確保不會出現這樣的情況,除非必要的情況下,一般最好不要直接對__dict__ 進行寫入操作,甚至不要直接操作__dict__

所以有個說法是 Python is a "consenting adults language"。

這種動態的實作使得我們的程式碼非常靈活,很多時候非常的便利,但這也付出了儲存和效能上的開銷。所以 Python 也提供了另一個機制(__slots__)來放棄使用 __dict__,以節約內存,提高性能,詳見 __slots__ 一節。

類別屬性

同樣的,類別屬性也在儲存在類別的__dict__ 字典中:

>>> Employee.__dict__
mappingproxy({'__module__': '__main__',              'outsource': True,              '__init__': <function>,              'inservice': <property>,              '__repr__': <function>,              '__str__': <function>,              '__dict__': <attribute>,              '__weakref__': <attribute>,              '__doc__': None}>>> type(Employee.__dict__)
mappingproxy复制代码</attribute></attribute></function></function></property></function>

與實例字典的「開放」不同,類別屬性使用的字典是一個MappingProxyType 對象,它是一個不能setattr 的字典。這意味著它對開發者是唯讀的,其目的正是為了保證類別屬性的鍵都是字串,以簡化和加快新型類別屬性的查找和 __mro__ 的搜尋邏輯。

>>> Employee.__dict__['outsource'] = FalseTypeError: 'mappingproxy' object does not support item assignment复制代码

因為所有的方法都歸屬於一個類,所以它們也儲存在類別的字典中,從上面的例子中可以看到已有的__init____repr__ 方法。我們可以再增加幾個來驗證:

class Employee:
    # ...    @staticmethod
    def soo():
        pass    @classmethod
    def coo(cls):
        pass

    def foo(self):
        pass复制代码
>>> Employee.__dict__
mappingproxy({'__module__': '__main__',              'outsource': False,              '__init__': <function>,              '__repr__': <function>,              'inservice': <property>,              'soo': <staticmethod>,              'coo': <classmethod>,              'foo': <function>,              '__dict__': <attribute>,              '__weakref__': <attribute>,              '__doc__': None})复制代码</attribute></attribute></function></classmethod></staticmethod></property></function></function>

繼承與屬性查找

目前為止,我們已經知道,所有的屬性和方法都儲存在兩個__dict__#字典中,現在我們來看看Python 是如何進行屬性查找的。

Python 3 中,所有類別都隱含的繼承自object,所以總是會有一個繼承關係,而且Python 是支援多重繼承的:

>>> class A:...     pass...>>> class B:...     pass...>>> class C(B):...     pass...>>> class D(A, C):...     pass...>>> D.mro()
[<class>, <class>, <class>, <class>, <class>]复制代码</class></class></class></class></class>

#mro() 是一個特殊的方法,它會傳回類別的線性解析順序。

屬性存取的預設行為是從物件的字典中取得、設定或刪除屬性,例如對於 e.f 的查找簡單描述是:

e.f 的查找顺序会从 e.__dict__['f'] 开始,然后是 type(e).__dict__['f'],接下来依次查找 type(e) 的基类(__mro__ 顺序,不包括元类)。 如果找到的值是定义了某个描述器方法的对象,则 Python 可能会重载默认行为并转而发起调用描述器方法。这具体发生在优先级链的哪个环节则要根据所定义的描述器方法及其被调用的方式来决定。

所以,要理解查找的顺序,你必须要先了解描述器协议。

简单总结,有两种描述器类型:数据描述器和和非数据描述器。

如果一个对象除了定义 __get__() 之外还定义了 __set__()__delete__(),则它会被视为数据描述器。仅定义了 __get__() 的描述器称为非数据描述器(它们通常被用于方法,但也可以有其他用途)

由于函数只实现 __get__,所以它们是非数据描述器。

Python 的对象属性查找顺序如下:

  1. 类和父类字典的数据描述器
  2. 实例字典
  3. 类和父类字典中的非数据描述器

请记住,无论你的类有多少个继承级别,该类对象的实例字典总是存储了所有的实例变量,这也是 super 的意义之一。

下面我们尝试用伪代码来描述查找顺序:

def get_attribute(obj, name):
    class_definition = obj.__class__

    descriptor = None
    for cls in class_definition.mro():        if name in cls.__dict__:
            descriptor = cls.__dict__[name]            break

    if hasattr(descriptor, '__set__'):        return descriptor, 'data descriptor'

    if name in obj.__dict__:        return obj.__dict__[name], 'instance attribute'

    if descriptor is not None:        return descriptor, 'non-data descriptor'
    else:        raise AttributeError复制代码
>>> e = Employee('IT', 'bobo')>>> get_attribute(e, 'outsource')
(False, 'non-data descriptor')>>> e.outsource = True>>> get_attribute(e, 'outsource')
(True, 'instance attribute')>>> get_attribute(e, 'name')
('bobo', 'instance attribute')>>> get_attribute(e, 'inservice')
(<property>, 'data descriptor')>>> get_attribute(e, 'foo')
(<function>, 'non-data descriptor')复制代码</function></property>

由于这样的优先级顺序,所以实例是不能重载类的数据描述器属性的,比如 property 属性:

>>> class Manager(Employee):...     def __init__(self, *arg):...         self.inservice = True...         super().__init__(*arg)
...>>> m = Manager("HR", "cici")
AttributeError: can't set attribute复制代码

发起描述器调用

上面讲到,在查找属性时,如果找到的值是定义了某个描述器方法的对象,则 Python 可能会重载默认行为并转而发起描述器方法调用。

描述器的作用就是绑定对象属性,我们假设 a 是一个实现了描述器协议的对象,对 e.a 发起描述器调用有以下几种情况:

  • 直接调用:用户级的代码直接调用e.__get__(a),不常用
  • 实例绑定:绑定到一个实例,e.a 会被转换为调用: type(e).__dict__['a'].__get__(e, type(e))
  • 类绑定:绑定到一个类,E.a 会被转换为调用: E.__dict__['a'].__get__(None, E)

在继承关系中进行绑定时,会根据以上情况和 __mro__ 顺序来发起链式调用。

函数与方法

我们知道方法是属于特定类的函数,唯一的不同(如果可以算是不同的话)是方法的第一个参数往往是为类或实例对象保留的,在 Python 中,我们约定为 clsself, 当然你也可以取任何名字如 this(只是最好不要这样做)。

上一节我们知道,函数实现了 __get__() 方法的对象,所以它们是非数据描述器。在 Python 访问(调用)方法支持中正是通过调用 __get__() 将调用的函数绑定成方法的。

在纯 Python 中,它的工作方式如下(示例来自描述器使用指南):

class Function:
    def __get__(self, obj, objtype=None):
        if obj is None:            return self        return types.MethodType(self, obj) # 将函数绑定为方法复制代码

在 Python 2 中,有两种方法: unbound method 和 bound method,在 Python 3 中只有后者。

bound method 与它们绑定的类或实例数据相关联:

>>> Employee.coo
<bound>>
>>> Employee.foo<function>
>>> e = Employee('IT', 'bobo')
>>> e.foo<bound>>复制代码</bound></function></bound>

我们可以从方法来访问实例与类:

>>> e.foo.__self__
<employee:>>>> e.foo.__self__.__class__
__main__.Employee复制代码</employee:>

借助描述符协议,我们可以在类的外部作用域手动绑定一个函数到方法,以访问类或实例中的数据,我将以这个示例来解释当你的对象访问(调用)类字典中存储的函数时将其绑定成方法(执行)的过程

现有以下函数:

>>> def f1(self):...     if isinstance(self, type):...         return self.outsource...     return self.name
...>>> bound_f1 = f1.__get__(e, Employee) # or bound_f1 = f1.__get__(e)>>> bound_f1
<bound>>>>> bound_f1.__self__
<employee:>>>> bound_f1()'bobo'复制代码</employee:></bound>

总结一下:当我们调用 e.foo() 时,首先从 Employee.__dict__['foo'] 中得到 foo 函数,在调用该函数的 foo 方法 foo.__get__(e) 将其转换成方法,然后执行 foo() 获得结果。这就完成了 e.foo() -> f(e) 的过程。

如果你对我的解释感到疑惑,我建议你可以阅读官方的描述器使用指南以进一步了解描述器协议,在该文的函数和方法和静态方法和类方法一节中详细了解函数绑定为方法的过程。同时在 Python 类一文的方法对象一节中也有相关的解释。

__slots__

Python 的对象属性值都是采用字典存储的,当我们处理数成千上万甚至更多的实例时,内存消耗可能是一个问题,因为字典哈希表的实现,总是为每个实例创建了大量的内存。所以 Python 提供了一种 __slots__ 的方式来禁用实例使用 __dict__,以优化此问题。

通过 __slots__ 来指定属性后,会将属性的存储从实例的 __dict__ 改为类的 __dict__ 中:

class Test:
    __slots__ = ('a', 'b')    def __init__(self, a, b):
        self.a = a
        self.b = b复制代码
>>> t = Test(1, 2)>>> t.__dict__
AttributeError: 'Test' object has no attribute '__dict__'>>> Test.__dict__
mappingproxy({'__module__': '__main__',              '__slots__': ('a', 'b'),              '__init__': <function>,              'a': <member>,              'b': <member>,              '__doc__': None})复制代码</member></member></function>

关于 __slots__ 我之前专门写过一篇文章分享过,感兴趣的同学请移步理解 Python 类属性 __slots__ 一文。

补充

__getattribute__ 和 __getattr__

也许你还有疑问,那函数的 __get__ 方法是怎么被调用的呢,这中间过程是什么样的?

在 Python 中 一切皆对象,所有对象都有一个默认的方法 __getattribute__(self, name)

该方法会在我们使用 . 访问 obj 的属性时会自动调用,为了防止递归调用,它总是实现为从基类 object 中获取 object.__getattribute__(self, name), 该方法大部分情况下会默认从 self__dict__ 字典中查找 name(除了特殊方法的查找)。

话外:如果该类还实现了 __getattr__则只有 __getattribute__ 显式地调用或是引发了 AttributeError 异常后才会被调用__getattr__ 由开发者自己实现,应当返回属性值或引发 AttributeError 异常。

而描述器正是由 __getattribute__() 方法调用,其大致逻辑为:

def __getattribute__(self, key):
    v = object.__getattribute__(self, key)    if hasattr(v, '__get__'):        return v.__get__(self)    return v复制代码

请注意:重写 __getattribute__() 会阻止描述器的自动调用。

函数属性

函数也是 Python function 对象,所以一样,它也具有任意属性,这有时候是有用的,比如实现一个简单的函数调用跟踪装饰器:

def calltracker(func):    @wraps(func)
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        return func(*args, **kwargs)
    wrapper.calls = 0
    return wrapper@calltrackerdef f():
    return 'f called'复制代码
>>> f.calls0>>> f()'f called'>>> f.calls1复制代码

相关免费学习推荐:python视频教程

以上是一起深入 Python 類別的內部的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:juejin。如有侵權,請聯絡admin@php.cn刪除
學習Python:2小時的每日學習是否足夠?學習Python:2小時的每日學習是否足夠?Apr 18, 2025 am 12:22 AM

每天學習Python兩個小時是否足夠?這取決於你的目標和學習方法。 1)制定清晰的學習計劃,2)選擇合適的學習資源和方法,3)動手實踐和復習鞏固,可以在這段時間內逐步掌握Python的基本知識和高級功能。

Web開發的Python:關鍵應用程序Web開發的Python:關鍵應用程序Apr 18, 2025 am 12:20 AM

Python在Web開發中的關鍵應用包括使用Django和Flask框架、API開發、數據分析與可視化、機器學習與AI、以及性能優化。 1.Django和Flask框架:Django適合快速開發複雜應用,Flask適用於小型或高度自定義項目。 2.API開發:使用Flask或DjangoRESTFramework構建RESTfulAPI。 3.數據分析與可視化:利用Python處理數據並通過Web界面展示。 4.機器學習與AI:Python用於構建智能Web應用。 5.性能優化:通過異步編程、緩存和代碼優

Python vs.C:探索性能和效率Python vs.C:探索性能和效率Apr 18, 2025 am 12:20 AM

Python在開發效率上優於C ,但C 在執行性能上更高。 1.Python的簡潔語法和豐富庫提高開發效率。 2.C 的編譯型特性和硬件控制提升執行性能。選擇時需根據項目需求權衡開發速度與執行效率。

python在行動中:現實世界中的例子python在行動中:現實世界中的例子Apr 18, 2025 am 12:18 AM

Python在現實世界中的應用包括數據分析、Web開發、人工智能和自動化。 1)在數據分析中,Python使用Pandas和Matplotlib處理和可視化數據。 2)Web開發中,Django和Flask框架簡化了Web應用的創建。 3)人工智能領域,TensorFlow和PyTorch用於構建和訓練模型。 4)自動化方面,Python腳本可用於復製文件等任務。

Python的主要用途:綜合概述Python的主要用途:綜合概述Apr 18, 2025 am 12:18 AM

Python在數據科學、Web開發和自動化腳本領域廣泛應用。 1)在數據科學中,Python通過NumPy、Pandas等庫簡化數據處理和分析。 2)在Web開發中,Django和Flask框架使開發者能快速構建應用。 3)在自動化腳本中,Python的簡潔性和標準庫使其成為理想選擇。

Python的主要目的:靈活性和易用性Python的主要目的:靈活性和易用性Apr 17, 2025 am 12:14 AM

Python的靈活性體現在多範式支持和動態類型系統,易用性則源於語法簡潔和豐富的標準庫。 1.靈活性:支持面向對象、函數式和過程式編程,動態類型系統提高開發效率。 2.易用性:語法接近自然語言,標準庫涵蓋廣泛功能,簡化開發過程。

Python:多功能編程的力量Python:多功能編程的力量Apr 17, 2025 am 12:09 AM

Python因其簡潔與強大而備受青睞,適用於從初學者到高級開發者的各種需求。其多功能性體現在:1)易學易用,語法簡單;2)豐富的庫和框架,如NumPy、Pandas等;3)跨平台支持,可在多種操作系統上運行;4)適合腳本和自動化任務,提升工作效率。

每天2小時學習Python:實用指南每天2小時學習Python:實用指南Apr 17, 2025 am 12:05 AM

可以,在每天花費兩個小時的時間內學會Python。 1.制定合理的學習計劃,2.選擇合適的學習資源,3.通過實踐鞏固所學知識,這些步驟能幫助你在短時間內掌握Python。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
1 個月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
1 個月前By尊渡假赌尊渡假赌尊渡假赌
威爾R.E.P.O.有交叉遊戲嗎?
1 個月前By尊渡假赌尊渡假赌尊渡假赌

熱工具

VSCode Windows 64位元 下載

VSCode Windows 64位元 下載

微軟推出的免費、功能強大的一款IDE編輯器

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

WebStorm Mac版

WebStorm Mac版

好用的JavaScript開發工具

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版