python内部类和外部类,

  python内部类和外部类,

  

python视频教程栏目介绍认识Python类的内部。

  本文将和大家谈谈Python 3.8中类和对象背后的一些概念和实现原理,主要是试图解释Python类和对象的存储、函数和方法、描述符、对象内存占用的优化支持、继承和属性查找。

  让我们从一个简单的例子开始:

  员工:类

  外包=错误

  def __init__(本人,部门,姓名):

  自我部门=部门

  self.name=name @property

  在职定义(自我):

  返回自己。部门不是无

  def __repr__(self):

  return employee : { self . department }-{ self . name } Employee=Employee( it , bobo )复制代码Employee对象是Employee类的一个实例,它有两个属性department和name,它们的值属于这个实例。外包是一个类属性,拥有者是一个类。类的所有实例对象都共享这个属性值,这与其他面向对象的语言是一致的。

  更改类变量会影响该类的所有实例对象:

  e1=员工( IT , bobo ),e2=员工( HR , cici ),e1 .外包,e2 .外包

  (false,false)employee . outsource=true 1 . outsource,e2.outsource (true,true)复制代码。这仅限于从类中更改。当我们从实例中改变类变量时:

  e1=员工( IT , bobo ),e2=员工( HR , cici ),e1 .外包,e2 .外包

  (错,错)e1.outsource=True e1.outsource,e2.outsource

  (对,错)复制代码是的,当你试图从实例对象修改类变量时,Python不会改变类的类变量值,而是创建一个同名的实例属性,这是非常正确和安全的。当搜索属性值时,实例变量将优先于类变量,这将在继承和属性查找一节中详细解释。

  值得注意的是,当类变量的类型是可变类型,时,您可以从实例对象中更改它们:

  S:类.L=[1,2]

  .s1,s2=S(),S() s1。l,s2。L

  ([1,2],[1,2]) t1。附加(3) t1。l,s2。L

  ([1,2,3],[1,2,3])复制代码的好方法是尽量避免这样的设计。

  

属性的存储

本节我们来看看Python中的类属性、方法、实例属性是如何关联存储的。

  00-1010在Python中,所有的实例属性都存储在__dict__ dictionary中,这是一个常规的dict。实例属性的维护就是从这个字典中获取和修改,这个字典对开发者是完全开放的。

  e=员工( 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__ 的完全开放,所以我们可以向其中添加任何 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 __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 的查找简单描述是:

  

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 '__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 类一文的方法对象一节中也有相关的解释。

  

__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 'a' of 'Test' objects>, 'b': <member 'b' of 'Test' 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 类的内部的详细内容,更多请关注盛行IT软件开发工作室其它相关文章!

  

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: