ホームページ > バックエンド開発 > Python チュートリアル > Python クラスの内部を見てみましょう

Python クラスの内部を見てみましょう

coldplay.xixi
リリース: 2020-11-18 17:16:00
転載
1704 人が閲覧しました

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: {self.department}-{self.name}>"employee = Employee('IT', 'bobo')复制代码
ログイン後にコピー

employee このオブジェクトは Employee クラスのインスタンスであり、2 つのプロパティがあります部門name。その値はこのインスタンスに属します。 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)复制代码
ログイン後にコピー

Yes はい、インスタンス オブジェクトからクラス変数を変更しようとすると、Python はクラスのクラス変数値を変更しませんが、同じ名前のインスタンス プロパティを作成します。これは非常に正確で安全です。 「継承とプロパティの検索」セクションで詳しく説明されているように、プロパティ値を検索するときは、インスタンス変数がクラス変数よりも優先されます。

クラス変数の型が mutable type の場合、インスタンス オブジェクトから変数を変更することに注意することが重要です:

>>> 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: IT-bobo>>>> state = e1.__dict__.copy()>>> del e1>>> e2 = new_employee_from(state)>>> e2>>> <Employee: IT-bobo>复制代码
ログイン後にコピー

__dict__ は完全にオープンであるため、数値などの任意の hashable immutable key を追加できます。

>>> e.__dict__[1] = 1>>> e.__dict__
{'department': 'IT', 'name': 'bobo', 1: 1}复制代码
ログイン後にコピー

これらの非文字列フィールドには、インスタンス オブジェクトを通じてアクセスすることはできません。そのような状況が発生しないようにするために、一般的には、必要な場合を除き、__dict__ に直接書き込まないことをお勧めします。 __dict__ を直接入力します。

つまり、Python は「同意する大人の言語」であるということです。

この動的な実装により、コードは非常に柔軟になり、多くの場合非常に便利になりますが、ストレージとパフォーマンスのオーバーヘッドも伴います。したがって、Python には、メモリを節約しパフォーマンスを向上させるために __dict__ の使用を放棄する別のメカニズム (__slots__) も用意されています。詳細については、__slots__ セクションを参照してください。

クラス属性

同様に、クラス属性もクラスの __dict__ 辞書に保存されます。

>>> 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复制代码
ログイン後にコピー

は、クラスの "open" とは異なります。インスタンス ディクショナリ 、クラス属性によって使用されるディクショナリは 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 __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})复制代码
ログイン後にコピー

継承と属性の検索

これまでのところ、すべての属性とメソッドが 2 つの __dict__ に格納されていることはすでにわかっています。辞書を使って、Python が属性検索をどのように実行するかを見てみましょう。

Python 3 では、すべてのクラスが object から暗黙的に継承されるため、常に継承関係が存在し、Python は多重継承をサポートしています。

>>> 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;>]复制代码
ログイン後にコピー

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 at 0x10c966d10>, 'data descriptor')>>> get_attribute(e, 'foo')
(<function __main__.Employee.foo(self)>, 'non-data descriptor')复制代码
ログイン後にコピー

由于这样的优先级顺序,所以实例是不能重载类的数据描述器属性的,比如 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 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>>复制代码
ログイン後にコピー

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

>>> e.foo.__self__
<Employee: IT-bobo>>>> e.foo.__self__.__class__
__main__.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 method f1 of <Employee: IT-bobo>>>>> bound_f1.__self__
<Employee: IT-bobo>>>> bound_f1()'bobo'复制代码
ログイン後にコピー

总结一下:当我们调用 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 __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})复制代码
ログイン後にコピー

关于 __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 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:juejin.im
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート