Plongeons dans les composants internes des classes Python

coldplay.xixi
Libérer: 2020-11-18 17:16:00
avant
1669 Les gens l'ont consulté

La colonne

tutoriel vidéo Python présente les éléments internes des classes Python.

Plongeons dans les composants internes des classes Python

Cet article parlera avec vous de certains concepts et principes d'implémentation derrière les classes et les objets dans Python 3.8. Il tente principalement d'expliquer les propriétés des classes et des objets Python. . Prise en charge de l'optimisation du stockage, des fonctions et méthodes, des descripteurs, de l'utilisation de la mémoire objet et des problèmes associés tels que l'héritage et la recherche d'attributs.

Commençons par un exemple simple : L'objet

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: {self.department}-{self.name}>"employee = Employee('IT', 'bobo')复制代码
Copier après la connexion

employee est une instance de la classe Employee et il possède deux propriétés department et name dont les valeurs appartiennent à cette instance. outsource est un attribut de classe, le propriétaire est la classe et tous les objets d'instance de la classe partagent cette valeur d'attribut, ce qui est cohérent avec les autres langages orientés objet.

La modification d'une variable de classe affectera tous les objets d'instance de la classe :

>>> e1 = Employee('IT', 'bobo')>>> e2 = Employee('HR', 'cici')>>> e1.outsource, e2.outsource
(False, False)>>> Employee.outsource = True>>> e1.outsource, e2.outsource>>> (True, True)复制代码
Copier après la connexion

Ceci est uniquement pour les modifications de la classe, lorsque nous modifions la variable de classe de l'instance :

>>> e1 = Employee('IT', 'bobo')>>> e2 = Employee('HR', 'cici')>>> e1.outsource, e2.outsource
(False, False)>>> e1.outsource = True>>> e1.outsource, e2.outsource
(True, False)复制代码
Copier après la connexion

Oui Oui, lorsque vous essayez de modifier une variable de classe à partir d'un objet instance, Python ne modifie pas la valeur de la variable de classe de la classe, mais crée une propriété d'instance avec le même nom, ce qui est très correct et sûr. Les variables d'instance ont la priorité sur les variables de classe lors de la recherche de valeurs de propriété, comme expliqué en détail dans la section Héritage et recherche de propriétés.

Il est important de noter que lorsque le type des variables de classe est type mutable, vous les changez depuis l'objet instance :

>>> 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])复制代码
Copier après la connexion

Bonne pratique La méthode est à éviter une telle conception autant que possible.

Stockage des attributs

Dans cette section, nous examinerons comment les attributs de classe, les méthodes et les attributs d'instance en Python sont associés et stockés.

Attributs d'instance

En Python, tous les attributs d'instance sont stockés dans le dictionnaire __dict__ Il s'agit d'un dict régulier. La maintenance des attributs d'instance se fait à partir de ce dictionnaire. , il est totalement ouvert aux développeurs.

>>> 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'复制代码
Copier après la connexion

Les attributs d'instance étant stockés dans un dictionnaire, nous pouvons facilement ajouter ou supprimer des champs à l'objet à tout moment :

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

Nous pouvons également instancier un objet à partir du dictionnaire, ou restaurer le instance en enregistrant le __dict__ de l'instance.

>>> 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: IT-bobo>>>> state = e1.__dict__.copy()>>> del e1>>> e2 = new_employee_from(state)>>> e2>>> <Employee: IT-bobo>复制代码
Copier après la connexion

Parce que __dict__ est complètement ouvert, nous pouvons y ajouter n'importe quel hashable clé immuable, comme des nombres :

>>> e.__dict__[1] = 1>>> e.__dict__
{'department': 'IT', 'name': 'bobo', 1: 1}复制代码
Copier après la connexion

ces Non Les champs -string ne sont pas accessibles via des objets d'instance. Afin de garantir qu'une telle situation ne se produise pas, il est généralement préférable de ne pas écrire directement dans __dict__ ou même d'opérer directement __dict__ sauf si cela est nécessaire.

Il y a donc un dicton selon lequel Python est un "langage pour adultes consentants".

Cette implémentation dynamique rend notre code très flexible et très pratique dans de nombreux cas, mais cela se fait également au détriment du stockage et des performances. Par conséquent, Python fournit également un autre mécanisme (__slots__) pour abandonner l'utilisation de __dict__ afin d'économiser de la mémoire et d'améliorer les performances. Voir la section __slots__ pour plus de détails.

Attributs de classe

De même, les attributs de classe sont également stockés dans le __dict__ dictionnaire de la classe :

>>> Employee.__dict__
mappingproxy({'__module__': '__main__',              'outsource': True,              '__init__': <function __main__.Employee.__init__(self, department, name)>,              'inservice': <property at 0x108419ea0>,              '__repr__': <function __main__.Employee.__repr__(self)>,              '__str__': <function __main__.Employee.__str__(self)>,              '__dict__': <attribute &#39;__dict__&#39; of &#39;Employee&#39; objects>,              '__weakref__': <attribute &#39;__weakref__&#39; of &#39;Employee&#39; objects>,              '__doc__': None}>>> type(Employee.__dict__)
mappingproxy复制代码
Copier après la connexion

Différent du "open" du dictionnaire d'instance, classe utilisation des attributs Le dictionnaire est un objet MappingProxyType, qui est un dictionnaire qui ne peut pas être setattr . Cela signifie qu'il est en lecture seule pour les développeurs et son objectif est de garantir que les clés des attributs de classe sont toutes des chaînes pour simplifier et accélérer la recherche de nouveaux attributs de classe et la logique de recherche de __mro__.

>>> Employee.__dict__['outsource'] = FalseTypeError: 'mappingproxy' object does not support item assignment复制代码
Copier après la connexion

Étant donné que toutes les méthodes appartiennent à une classe, elles sont également stockées dans le dictionnaire de classe. À partir de l'exemple ci-dessus, vous pouvez voir les méthodes __init__ et __repr__ existantes. Nous pouvons en ajouter quelques autres pour vérifier :

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

    def foo(self):
        pass复制代码
Copier après la connexion
>>> Employee.__dict__
mappingproxy({'__module__': '__main__',              'outsource': False,              '__init__': <function __main__.Employee.__init__(self, department, name)>,              '__repr__': <function __main__.Employee.__repr__(self)>,              'inservice': <property at 0x108419ea0>,              'soo': <staticmethod at 0x1066ce588>,              'coo': <classmethod at 0x1066ce828>,              'foo': <function __main__.Employee.foo(self)>,              '__dict__': <attribute &#39;__dict__&#39; of &#39;Employee&#39; objects>,              '__weakref__': <attribute &#39;__weakref__&#39; of &#39;Employee&#39; objects>,              '__doc__': None})复制代码
Copier après la connexion

Héritage et recherche d'attributs

Jusqu'à présent, nous savons déjà que tous les attributs et méthodes sont stockés dans deux __dict__ dictionnaires, prenons maintenant un regardez comment Python effectue la recherche d'attributs.

Dans Python 3, toutes les classes héritent implicitement de object, il y a donc toujours une relation d'héritage, et Python prend en charge l'héritage multiple :

>>> class A:...     pass...>>> class B:...     pass...>>> class C(B):...     pass...>>> class D(A, C):...     pass...>>> D.mro()
[<class &#39;__main__.D&#39;>, <class &#39;__main__.A&#39;>, <class &#39;__main__.C&#39;>, <class &#39;__main__.B&#39;>, <class &#39;object&#39;>]复制代码
Copier après la connexion

mro() est une méthode spéciale qui renvoie le ordre d'analyse linéaire de la classe. Le comportement par défaut de l'accès aux attributs

consiste à obtenir, définir ou supprimer des attributs du dictionnaire de l'objet. Par exemple, la description simple de la recherche de e.f est :

.

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复制代码
Copier après la connexion
>>> 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 at 0x10c966d10>, 'data descriptor')>>> get_attribute(e, 'foo')
(<function __main__.Employee.foo(self)>, 'non-data descriptor')复制代码
Copier après la connexion

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

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

发起描述器调用

上面讲到,在查找属性时,如果找到的值是定义了某个描述器方法的对象,则 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) # 将函数绑定为方法复制代码
Copier après la connexion

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

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

>>> Employee.coo
<bound method Employee.coo of <class &#39;__main__.Employee&#39;>>
>>> Employee.foo<function __main__.Employee.foo(self)>
>>> e = Employee('IT', 'bobo')
>>> e.foo<bound method Employee.foo of <Employee: IT-bobo>>复制代码
Copier après la connexion

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

>>> e.foo.__self__
<Employee: IT-bobo>>>> e.foo.__self__.__class__
__main__.Employee复制代码
Copier après la connexion

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

现有以下函数:

>>> 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 method f1 of <Employee: IT-bobo>>>>> bound_f1.__self__
<Employee: IT-bobo>>>> bound_f1()'bobo'复制代码
Copier après la connexion

总结一下:当我们调用 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复制代码
Copier après la connexion
>>> t = Test(1, 2)>>> t.__dict__
AttributeError: 'Test' object has no attribute '__dict__'>>> Test.__dict__
mappingproxy({'__module__': '__main__',              '__slots__': ('a', 'b'),              '__init__': <function __main__.Test.__init__(self, a, b)>,              'a': <member &#39;a&#39; of &#39;Test&#39; objects>,              'b': <member &#39;b&#39; of &#39;Test&#39; objects>,              '__doc__': None})复制代码
Copier après la connexion

关于 __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复制代码
Copier après la connexion

请注意:重写 __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'复制代码
Copier après la connexion
>>> f.calls0>>> f()'f called'>>> f.calls1复制代码
Copier après la connexion

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:juejin.im
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal