この記事では、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 '__dict__' of 'Employee' objects>, '__weakref__': <attribute '__weakref__' of 'Employee' 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 '__dict__' of 'Employee' objects>, '__weakref__': <attribute '__weakref__' of 'Employee' 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 '__main__.D'>, <class '__main__.A'>, <class '__main__.C'>, <class '__main__.B'>, <class 'object'>]复制代码
mro()
は、クラスの線形解析順序を返す特別なメソッドです。
属性アクセスのデフォルトの動作は、オブジェクトの辞書から属性を取得、設定、または削除することです。たとえば、e.f
の検索の簡単な説明は次のとおりです:
e.f
的查找顺序会从e.__dict__['f']
开始,然后是type(e).__dict__['f']
,接下来依次查找type(e)
的基类(__mro__
顺序,不包括元类)。 如果找到的值是定义了某个描述器方法的对象,则 Python 可能会重载默认行为并转而发起调用描述器方法。这具体发生在优先级链的哪个环节则要根据所定义的描述器方法及其被调用的方式来决定。
所以,要理解查找的顺序,你必须要先了解描述器协议。
简单总结,有两种描述器类型:数据描述器和和非数据描述器。
如果一个对象除了定义
__get__()
之外还定义了__set__()
或__delete__()
,则它会被视为数据描述器。仅定义了__get__()
的描述器称为非数据描述器(它们通常被用于方法,但也可以有其他用途)
由于函数只实现 __get__
,所以它们是非数据描述器。
Python 的对象属性查找顺序如下:
请记住,无论你的类有多少个继承级别,该类对象的实例字典总是存储了所有的实例变量,这也是 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 中,我们约定为 cls
或 self
, 当然你也可以取任何名字如 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 '__main__.Employee'>> >>> 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 类一文的方法对象一节中也有相关的解释。
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 'a' of 'Test' objects>, 'b': <member 'b' of 'Test' objects>, '__doc__': None})复制代码
关于 __slots__ 我之前专门写过一篇文章分享过,感兴趣的同学请移步理解 Python 类属性 __slots__ 一文。
也许你还有疑问,那函数的 __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 サイトの他の関連記事を参照してください。