이 기사에서는 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
클래스의 인스턴스이며 department
와 department
라는 두 가지 속성을 갖습니다. 값이 이 인스턴스에 속하는 이름
입니다. outsource
는 클래스 속성이고 소유자는 클래스입니다. 클래스의 모든 인스턴스 객체는 이 속성의 값을 공유하며 이는 다른 객체 지향 언어와 일치합니다. employee
对象是 Employee
类的一个实例,它有两个属性 department
和 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)复制代码
是的,当你试图从实例对象修改类变量时,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: 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 is a "consenting adults language"。
这种动态的实现使得我们的代码非常灵活,很多时候非常的便利,但这也付出了存储和性能上的开销。所以 Python 也提供了另外一种机制(__slots__
)来放弃使用 __dict__
,以节约内存,提高性能,详见 __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复制代码
与实例字典的『开放』不同,类属性使用的字典是一个 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})复制代码
目前为止,我们已经知道,所有的属性和方法都存储在两个 __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
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')复制代码🎜 예, 클래스 변수를 다음에서 수정하려고 할 때 인스턴스 객체인 경우 Python은 클래스의 클래스 변수 값을 변경하지 않지만 동일한 이름을 가진 인스턴스 속성을 생성하므로 매우 정확하고 안전합니다. 상속 및 속성 조회 섹션에 자세히 설명된 대로 속성 값을 검색할 때 인스턴스 변수에 클래스 변수보다 우선순위가 부여됩니다. 🎜🎜클래스 변수 유형이 변수 유형인 경우 인스턴스 객체에서 변수 유형을 변경한다는 점에 특별한 주의를 기울일 필요가 있습니다. 🎜
>>> class Manager(Employee):... def __init__(self, *arg):... self.inservice = True... super().__init__(*arg) ...>>> m = Manager("HR", "cici") AttributeError: can't set attribute复制代码🎜좋은 습관은 가능한 한 이러한 유형을 피하는 것입니다. 🎜
dict
인 __dict__
사전에 저장됩니다. 인스턴스 속성의 유지 관리는 개발자에게 완전히 공개된 이 사전에서 속성을 얻고 수정하는 것입니다. 🎜class Function: def __get__(self, obj, objtype=None): if obj is None: return self return types.MethodType(self, obj) # 将函数绑定为方法复制代码🎜인스턴스 속성은 사전에 저장되므로 언제든지 객체에 필드를 쉽게 추가하거나 삭제할 수 있습니다. 🎜
>>> 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>>复制代码🎜또한 사전에서 객체를 인스턴스화하거나 인스턴스의
__dict__
를 저장할 수도 있습니다. > 인스턴스를 복원합니다. 🎜>>> e.foo.__self__ <Employee: IT-bobo>>>> e.foo.__self__.__class__ __main__.Employee复制代码🎜
__dict__
는 완전히 개방되어 있으므로 해싱 가능한 불변 키를 여기에 추가할 수 있습니다. 예를 들어 숫자는 다음과 같습니다. 🎜>>> 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'复制代码🎜이것은 그렇지 않습니다. 이러한 상황이 발생하지 않도록 하려면 일반적으로 필요한 경우가 아니면
__dict__
에 직접 쓰지 않거나 __dict__
. 🎜🎜그래서 파이썬은 "성인의 동의를 받는 언어"라는 말이 있습니다. 🎜🎜이 동적 구현은 많은 경우 코드를 매우 유연하고 편리하게 만들지 만 저장 및 성능이 저하됩니다. 따라서 Python은 메모리를 절약하고 성능을 향상시키기 위해
__dict__
사용을 포기하는 또 다른 메커니즘(__slots__
)도 제공합니다. 🎜__dict__
사전에 저장됩니다. 🎜class Test: __slots__ = ('a', 'b') def __init__(self, a, b): self.a = a self.b = b复制代码🎜"open"과 다릅니다. 인스턴스 사전의 클래스 특성에서 사용되는 사전은
setattr
일 수 없는 사전인 MappingProxyType
개체입니다. 이는 개발자를 위한 읽기 전용임을 의미하며, 그 목적은 클래스 속성의 키가 모두 문자열인지 확인하여 새 클래스 속성의 조회와 __mro__
의 검색 논리를 단순화하고 속도를 높이는 것입니다. . 🎜>>> 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})复制代码🎜모든 메소드는 클래스에 속하기 때문에 클래스 사전에도 저장되어 있습니다. 위의 예를 보면 기존
__init__
및 __repr__
메소드를 볼 수 있습니다. 확인을 위해 몇 가지를 더 추가할 수 있습니다: 🎜def __getattribute__(self, key): v = object.__getattribute__(self, key) if hasattr(v, '__get__'): return v.__get__(self) return v复制代码
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'复制代码
객체
에서 상속되므로 항상 상속 관계가 있고 Python은 다중 상속을 지원합니다. 🎜>>> f.calls0>>> f()'f called'>>> f.calls1复制代码🎜
mro() code>는 특수한 상속입니다. 클래스의 선형 구문 분석 순서를 반환하는 메서드입니다. 🎜🎜속성 액세스의 기본 동작은 개체 사전에서 속성을 가져오거나 설정하거나 삭제하는 것입니다. 예를 들어 <code>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 중국어 웹사이트의 기타 관련 기사를 참조하세요!