Python-class
2020/10/25更新 整合了内容
Python面向对象编程
ps:相对熟悉的知识就不仔细列出了
定义与实例
- class定义一个对象
- 类的实例是以函数的形式调用类对象来创建的,__init__()为构造函数,__del__()为析构函数
作用域规则
- Python类中没有作用域,这与C++,Java不同。而需要显示使用self的原因在于Python没有提供显示声明变量的方式(如:int x;),因此无法知道在方法中要赋值的变量是不是局部变量,或者是否要保存为实例属性,而显示self可以解决这一问题。
继承
- super(cls, instance)会返回一个特殊对象,该对象支持在基类上执行属性查找。你可以通过super获取基类,调用基类的函数。在Python3中,super中的参数可以不要。
- 多继承不要使用不要使用不要使用
多态动态绑定和鸭子类型
- 动态绑定(在继承背景下使用,也成多态),obj.attr,首先搜索实例本身,然后是实例的类定义,然后是基类,查找会返回第一个匹配项
- 动态绑定在于其不受对象obj的类型影响,因此如果执行像obj.name这样的查找,对于所有拥有name属性的obj都适用。这种行为有时候被称作“鸭子类型”(duck typing),这个名称来源于一个谚语:“如果看起来,叫声像而且走起路来像鸭子,那么它就是鸭子”。
静态方法和类方法
- 静态方法是一种普通函数,只不过它们正好位于类定义的命名空间中,它不会对任何实例进行操作。要定义静态方法,使用@staticmethod装饰器,同时调用静态方法只需要类名作为前缀cls.staticmethod
- 类方法是类本身作为对象进行操作的方法。使用@classmethod装饰器定义,类作为第一个参数传递,例如:
1 | class Times(): |
特性
- 通常,访问实例或类的属性时,返回的是存储的相关值。而特性(property)是一种特殊的属性,访问它时会计算它的值。
1 |
|
- 我们的area和perimeter并非是通过调用函数来计算所得(并没有调用area()),而是通过radius计算所得,结果作为类的一个属性,但是该属性不能被赋值。
- 特性还可以截获操作权,以设置和删除属性
- 下面例子使用property(getf=None, setf=None, delf=None, doc=None)来定义特性。
1 | # 定义一个可控属性值 x |
- 如果 c 是 C 的实例化, c.x 将触发 getter,c.x = value 将触发 setter , del c.x 触发 deleter。
- 如果给定 doc 参数,其将成为这个属性值的 docstring,否则 property 函数就会复制 fget 函数的 docstring(如果有的话)。
下面例子是另一种写法(推荐),将 property 函数用作装饰器可以很方便的创建只读属性:
1 | class Parrot(object): |
- 这个代码和第一个例子完全相同,但要注意这些额外函数的名字和property下的一样。
描述符
- 使用特性后,对对象的访问将通过一系列的用户定义的get,set,delete控制。这种属性将通过描述符对象进一步泛化。描述符就是一个代表属性值的对象。通过实现一个或多个特殊的__get__(), __set__(), __delete__()方法,可以将描述符和属性访问机制挂钩,也可以自定义这些操作。
1 |
|
- 个人的理解就是(以上例中的name),创建了一个实例,该实例通过getattr调用str类型的属性,通过调用str的setattr来设置属性。
- 描述符只能在类级别上进行实例化。不能通过在__init__()和其他方法中创建描述符对象来为每个对象创建描述符。持有描述符的类使用的属性名称比实例存储的属性名称有更高的优先级,描述符对象接受name时对其值略加修改(加了一个下划线),原因就在于此,为了让描述符在实例上存储值,描述符必须挑选一个与它本身所用名称不同的名称。(不懂2333)。
数据封装和私有属性
- object # public
- __object__ # special, python system use, user should not define like it
- __object # private (name mangling? during runtime)
- _object # obey python coding convention, consider it as private
核心风格:避免用下划线作为变量名的开始。
- 类中的所有已双下滑线开头的名称,无论属性还是方法,都会形成_Classname__xx形式的新名称,这样不会和基类的私有变量发生冲突
- 尽管这种方法似乎隐藏了数据,但没有严格的机制来实际阻止对类的“私有”属性的访问。特别是如果已知类的名称和相应的私有属性的名称,则可以使用变形后的名称来访问。通过重定义__dir__()方法,类可以降低这些属性的可见性,__dir__()方法提供了对象的dir()所返回的名称列表,如下:
1 | class Foo(): |
- 这就很神奇,明明我声明了私有变量,你在类外却可以访问,这显然破坏了私有性,不要使用。
1 |
|
我们对dir的返回结果切片,自定义了__dir__()的返回结果,这样我们就无法在类外知道类里究竟有无私有变量了(私有变量一般会放在最前面)
对象内存管理
- 我们定义类后得到的实际是一个可以创建新实例的工厂。
__init__&__new__
- 实例的创建包括2个步骤,使用特殊方法__new__()创建新的实例,然后使用__init__()初始化
1 | c = Circle.__new__(Circle, 4) |
- 类的__new__()方法很少通过用户代码定义。如果定义了它,它的原型
__new__(cls, *args, **kwargs)
所以,__init__ 和 __new__ 最主要的区别在于:
1.__init__ 通常用于初始化一个新实例,控制这个初始化的过程,比如添加一些属性, 做一些额外的操作,发生在类实例被创建完以后。它是实例级别的方法。
2.__new__ 通常用于控制生成一个新实例的过程。它是类级别的方法。
- 依照Python官方文档的说法,__new__方法主要是当你继承一些不可变的class时(比如int, str, tuple), 提供给你一个自定义这些类的实例化过程的途径。还有就是实现自定义的metaclass(元类)。
如:
1 | class UpperStr(str): |
对象管理
- 创建实例后,实例将由引用计数来管理。如果引用计数到达0,实例立即被销毁。当销毁时,解释器会先找与对象相关的__del__()并调用。实际上,很少定义该方法。唯一的例外是在销毁对象时需要执行操作的(如关闭文件,关闭网络连接或释放其他系统资源)。即使在这样的情况下,依靠__del__()来完全关闭依然存在风险。更好的方案是定义一个方法,如close(),程序可以使用该方法显示执行关闭操作。
- 有时,程序使用del语言删除对象引用。如果这导致引用计数为0,则会调用__del__(),但del通常不会直接调用__del__()
- 尽管定义__del__()很少会破坏垃圾回收器,但在某些编程模式下可能会引起问题,如:“观察者模式”(Observer Pattern)
1 |
|
-
看起来似乎没啥毛病,这段代码中Account类允许一组AccountObserver对象监控。每个Account会有一组观察者,而每个观察者会保留对账户的引用。
-
每个类都定义了__del__(),尝试清除(如注销),但是,这会建立一个引用循环,在这个循环中,引用计数永远不会到0,也永远不会执行清除操作,不仅如此,垃圾回收机器(gc机制)甚至不会清除该类,导致内存永久泄露。
-
解决方案就是使用弱引用,用一种在不增加引用计数的情况下创建对象的引用方式。
-
修改后的代码:
1 |
|
__slots__
- 通过定义特殊变量__slots__,类可以限制对合法实例属性名称的设置,如下:
1 | class Account(): |
- 定义__slot__时,可以将实例上分配的属性名称限制为指定名称,否则引发AttributeError异常。这可以阻止其他人向现有实例增加新属性,或者用户写出属性导致加入新的属性
- 但实际使用中,__slot__从未被当做一种安全特性来实现。它实际上是对内存和执行速度的一种性能优化。在会创建大量对象的程序中,__slot__的使用可以显著的减少内存占用和执行时间
对象表示和属性绑定
- 从内部实现上看,实例是用字典实现的,可以使用__dict__()属性访问该字典,这个字典包含的数据对每个实例而言是唯一的。对对象属性的修改会反映在__dict__()中,而直接修改__dict__()也会影响对象的属性。
1 | >>>a = Account("xkx", 1200) |
-
实例通过特殊属性__class__()链接回类。类本身也可以看做对字典的浅层包装。
>>>a.__class__
<class ‘__main__.Account’> -
特殊属性__base__()中将类链接到它们的基类,该属性是一个基类元组。这种底层结构是获取,设置,删除对象属性的所有操作的基础。
-
当使用obj.name = value时,就会调用特殊方法obj.__setattr__(“name”, value),删除时会调用__delattr__(“name”),这种默认的行为是修改或删除obj的局部的__dict__()的值。
-
查找属性时,obj.name, 将调用__getattribute__(“name”),会检查特性,局部__dict__,类字典,基类。如果该过程失败,则会调用类的__getattr__()方法(如果定义了)来查找,如果还是没有找到,就会抛出AttributeError异常。
-
一般类很少重新定义属性访问运算符,但在编写通用的包装器和现有对象代理时,会用的上。
元类
- 类对象的创建方式是由一种名为元类的特殊对象控制的,简而言之,元类就是知道如何去创建和管理类的对象。
1 | >>>class Foo(): pass |
- 这个例子,控制Foo创建的就是名为type的类
- 当使用class来定义新类时,将会发生很多事情。首先,类主体将作为其自己的私有字典内的一系列语句来执行。语句的执行和正常代码的执行一样,只是增加了会对私有成员发生名字变形。最后,类的名称,基类列表,字典将传给元类的构造函数,创建响应的类对象。下面例子演示了这一过程:
1 | class_name = "Foo" |
- 最后一步调用元类,该元类可以自己定义,如class Foo(mateclass=type)
- 如果没有显示指定元类,将检查基类元组,元类与第一个基类的类型相同。
- 如果没有指定基类,class将检查全局变量__metaclass__是否存在。如果有,就会使用其创建类。
- 最最后,使用默认元类,Python3默认为type()
- 例子,要求用户定义类的方法必须拥有一个文档字符串:
1 | class DocMeta(type): |
- 更高级的用法(看看就行了),元类可以在创建类前同时检查和更改类定义的内容,这需要重写__new__().
1 | class TypedProperty(): |
- part1的一个例子,不同的是我们设置self.name变成了在元类里设置。该例子中,元类扫描类字典,查找TypedProperty的实例,找到的话,设置name属性并在slots中建立名称列表。完成之后__slots__将添加到类字典中,通过type元类的__new__来构造。
类装饰器
- 有时不用小题大做弄元类来处理一些问题,只用简单的写个修饰器就行。如将类添加到注册表或数据库。
1 |
|
- 这个例子可以对类按自定义的功能划分,进行不同的后续处理(写不同的日志啥的)