22. 面向对象 - 高阶

閱讀時間約 45 分鐘
raw-image


Hi,大家好。我是茶桁。

之前的课程里面,我们简单的接触了面向对象编程,也和大家讲解了其思想,优缺点。相信上节课程结束之后,大家对面向对象都有了一定的理解。

那么我们这节课,就进入面向对象的一些高阶部分,让我们继续来学习一些魔术方法以及Python的内置成员,然后再来学习一下描述符与设计模式。

  1. 内置成员
  2. 魔术方法
  3. 描述符
  4. 设计模式

好,正课走起。让我们开始。

内置成员

当我们创建一个类之后,即便我们还什么都没做,这个类里面就已经有内容了,我们来看一下:

 class Demo():
     pass
 ​
 # 获取类/对象的所属成员
 res = Demo.__dict__
 print(res)
 ​
 ---
 {'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Demo' objects>, '__weakref__': <attribute '__weakref__' of 'Demo' objects>, '__doc__': None}

上节课我们学过了__dict__, 这个是获取类或者对象的成员的方法。打印结果我们看到其中的成员。

让我们添加些内容再来观察一下:

 class Demo():
     name = 'a'
     age = 20
 ​
     def say(self):
         print('say something')
 ​
 # 获取类/对象的所属成员
 res = Demo.__dict__
 print(res)
 ​
 ---
 {'__module__': '__main__', 'name': 'a', 'age': 20, 'say': <function Demo.say at 0x1117a3e20>, '__dict__': <attribute '__dict__' of 'Demo' objects>, '__weakref__': <attribute '__weakref__' of 'Demo' objects>, '__doc__': None}

看到我们刚才定义的成员属性和成员方法也都在列了。我们还可以实例化之后获取对象的成员:

 obj = Demo()
 print(obj.__dict__)
 ​
 ---
 {}

当我们实例化一个对象obj之后,打印发现其成员是空的。这是为什么?

原因就在于,这个方法用处其实是打印其对象的专有成员。我们再来为这个实例化对象创建一些成员再来看看:

 obj.sex = 'female'
 print(obj.__dict__)
 ​
 ---
 {'sex': 'female'}

可以看到,我们获取了刚才创建的成员属性。

以上,就是我们使用__dict__获取了类和对象的所属成员,方法为:类/对象.__dict__

除了获取所属成员,我们还有其他方法,比如获取「文档信息」, 获取「类名称」, 获取「所在文件名称」,获取「当前类的父类列表」以及获取「当前类的『继承链』。来让我们依次看一下:

还记得我们之前在创建函数的时候可以添加文档吗?

 def obj():
   '''
  这里是文档内容
  '''

同样的,类当中我们一样可以添加文档内容。然后我们可以通过__doc__来获取:

 class Demo():
     '''
    这里是一个Demo类,主要用于测试
    '''
     pass
 ​
 print(Demo.__doc__)
 ​
 ---
 这里是一个Demo类,主要用于测试

同样的,__doc__不仅可以获取类的文档信息,同样可以获取到对象的。

我们使用__name__来获取类名称「组成的字符串」, 这个方法无法对对象使用。

 print(Demo.__name__)
 ​
 ---
 Demo

__module__可以用来获取类/对象所在的文件名称

 print(Demo.__module__)
 print(obj.__module__)
 ​
 ---
 __main__
 __main__

如果其所在文件为当前文件,那么这里就会显示为__main__

然后是__base__, 这个方法是用来获取当前类的父类列表。这个方法有两个版本,一个是__base__, 一个是__bases__。这两个方法的区别在于一个是获取继承的第一个父类,一个是继承所有的父类的列表,为了呈现的更明显,我们建立一个继承类:

 class A(Demo):
     pass
 class B(A, Demo):
     pass
 ​
 print(B.__base__)
 print(B.__bases__)
 ​
 ---
 <class '__main__.A'>
 (<class '__main__.A'>, <class '__main__.Demo'>)

还有一个就是我们上节课讲过的,MRO列表,也就是__mro__方法,用于获取当前类的继承链。

 print(B.__mro__)
 ​
 ---
 (<class '__main__.B'>, <class '__main__.A'>, <class '__main__.Demo'>, <class 'object'>)

到此为止,我们介绍的就是常用的一些内置成员获取的一些方法。当然,这里不是全部,除此之外还有很多,因为并不是常用,所以这里我们就不多介绍了。

raw-image


方法的分类

接下来呢,我们来看下面向对象的分类,包括:

  1. 对象方法
  2. 类方法
  3. 绑定类方法
  4. 静态方法

对象方法

其特征为:

  1. 在类中定义方法,含有self参数
  2. 含有self的方法,只能使用对象进行调用。
  3. 该方法会把调用的对象传给进来。
 class Demo():
     # 对象方法
     def objFunc(self):
         print(self)
         print('this is objFunc')
 ​
 # 实例化对象
 obj = Demo()
 obj.objFunc()
 ​
 ---
 <__main__.Demo object at 0x1171460e0>
 this is objFunc

这个方法不能直接使用类直接调用,但是其实也不是绝对的。当我们使用类直接调用的时候,需要传递一个参数,也就是必须要self有参数可接收。

 Demo.objFunc('a')
 ​
 ---
 a
 this is objFunc

类方法

类方法呢,和对象方法有不一样的地方,也有相同的地方。两者定义十分相似,不同之处是使用装饰器材:

其特征为:

  1. 在类中定义的方法,使用了@classmethod进行了装饰
  2. 方法中有形参cls
  3. 可以不用实例化对象,直接使用类进行调用
  4. 会把调用这个方法的类或对象传递进来

来直接看代码理解:

 class Demo():
     
     # 类方法
     @classmethod # 装饰器
     def clsFunc(cls):
         print(cls)
         print('this is cls function: clsFunc')
 ​
 Demo.clsFunc()
 obj.clsFunc()
 ​
 ---
 <class '__main__.Demo'>
 this is cls function: clsFunc
 <class '__main__.Demo'>
 this is cls function: clsFunc

看结果可以看到,我们用类进行调用的时候并没有像对象方法一样传递一个参数进去,这是因为调用的时候会直接传递调用的类给到cls参数。而我们说不需要实例化对象,并不是实例化对象不可调用。对象调用也是可以的。

至于什么是「装饰器」,我们以后会详细讲到,这里先记住这种形式就可以了。

绑定类方法

这个方法不传递任何对象和类。在定义的时候,不设定任何的形参:

 class Demo():
 ​
     # 绑定类方法
     def bindClassFunc():
         print('this is bind Class function: bindClassFunc')
 ​
 # 调用
 Demo.bindClassFunc()
 ​
 ---
 this is bind Class function: bindClassFunc

那么绑定类方法既然没有定义形参,那么这个方法是无法使用实例化对象来调用的。

 obj.bindClassFunc()
 ​
 ---
 TypeError: Demo.bindClassFunc() takes 0 positional arguments but 1 was given

其特征如下:

  1. 在类中定义的方法,不必须设置形参。
  2. 只能使用类进行调用。
  3. 可以传递任意参数,但是不会将类作为参数传递进来。

静态方法

「静态类方法」和「类方法」相似,也需要一个装饰器。并且,静态类方法也是不需要设置形参的。

 class Demo():
     # 静态类方法
     @staticmethod
     def staticFunc():
         print('this is static method func')
 ​
 Demo.staticFunc()
 obj.staticFunc()
 ​
 ---
 this is static method func
 this is static method func

那从结果中我们可以看到,「静态类方法」可以使用类和对象进行调用,并且调用的时候不需要传递任何参数。

其特征如下:

  1. 在类中定义的方法,使用装饰器 @staticmethod 进行了装饰
  2. 可以使用对象或者类进行调用
  3. 不会将对象或者类作为参数传递进来

⚠️ 注意:这里我们需要注意的,「静态类方法」只是可以不设置参数,并不是不能设置参数,并且,就算是设置了参数之后,也是不接受类和对象作为参数传递的。比如:

 @staticmethod
 def staticFunc(a, b):
     print(f'a:{a}, b:{b}')
     print('this is static method func')
 ​
 # 调用
 Demo.staticFunc('static', 'class')
 obj.staticFunc('static', 'obj')
 ​
 ---
 a:static, b:class
 this is static method func
 a:static, b:obj
 this is static method func

我们分别用类和对象进行了调用并传递了两个参数进行打印,而打印结果正常,并且没有对象或者类被传递。

相应的,「绑定类方法」也是这种特性,只是「绑定类方法」只支持类调用,不支持对象调用。

常用函数

其实在之前,关于「常用函数」我们已经接触过了一些,比如:issubclass(子类,父类)。有些小伙伴可能还记得,这个函数是用于检测一个类是否为另一个类的子类。

那除了这个之外,Python中还有很多其他的一些针对类和对象的常用函数,下面让我们来详细看一下。

isinstance(对象,类), 用于检测一个对象是否是该类或者该类的子类的实例化结果。

obj = D()
print(isinstance(obj, D))

---
True

这个结果显而易见,那么我们思考一下,既然D类继承了B类和C类,那么obj对象是否也是B或者C的实例化结果呢?

print(isinstance(obj, B))

---
True

可见,对于继承了父类的子类,其实例化对象和父类之间也会被检测为True

hasattr(对象/类,'成员名称'), 这个函数是用于检测类/对象是否包含指定名称的成员。

B.name = '张三'
print(hasattr(obj, 'name'))

---
True

在这段代码中,我们给父类B添加了一个成员属性name,因为objD的实例化对象(之前的代码中)。而D类是继承自B类的,所以自然obj中也是包含了name这个成员属性的。所以我们的检测结果必然为True

来,我们做另外一个实验:

 objB = B()
 D.age = 20
 print(D.age)
 print(hasattr(objB, 'age'))
 ​
 ---
 20
 False

我们重新用B类实例化了一个对象objB, 然后我们给D类添加了一个成员属性age,并且打印了一遍证实其存在。这个时候我们检测了一下objB中是否含有age, 因为DB的子类,它说添加的成员属性为独有属性,并不会更改到B类里,那自然B的实例化对象objB中是不可能存在这个成员属性的,结果自然为False

``

getattr(对象/类,'成员名称'), 用于获取类/对象的成员的值

那这个函数就好理解了,我们可以用之前建立好的实例化对象objBobj来获取一下试试看:

 print(getattr(obj, 'age'))
 print(getattr(objB, 'name'))
 ​
 ---
 20
 张三

没问题,结果如我们所料一般。那如果是获取objB中的age的值会如何?我们前面已经知道,objB中并未存在age这个成员属性,所以必然会报错:

 print(getattr(objB, 'age'))
 ​
 ---
 AttributeError: 'B' object has no attribute 'age'

setattr(对象/类,'成员名称','成员的值'), 这个函数用于设置类/对象的成员的属性值。

 print(setattr(obj, 'name', 'du'))
 print(obj.name)
 ​
 ---
 None
 du

如结果所见,这个方法的返回值为None,但是我们通过打印obj.name可知,方法确实更改了objname的值。

delattr(类/对象,'成员名称') 这个函数可以删除类/对象的成员属性,和del直接删除对象的成员是一样的结果。

 print(delattr(obj, 'name'))
 print(obj.name)
 ​
 ---
 None
 张三

可见,这个方法也是没有返回值的,返回了None。不过,既然我们已经删除了obj中的name, 为啥还能打印出张三呢?有没有小伙伴知道为什么?其实,我们删除的name是之前使用setattrobj设定的专有成员,当它被删除之后,我们的obj的继承类D中还存在着name这个成员属性,所以现在打印出来的张三是从D类中继承过来的。

那如果是我们新添加的但是其他类中没有的属性就会直接报错了,来看:

 setattr(obj, 'size', 'small')
 print(obj.size)
 delattr(obj, 'size')
 print(obj.size)
 ​
 ---
 small
 AttributeError: 'D' object has no attribute 'size'

我们分别打印了两次,第一次setattr了一个成员属性size,并且打印验证了。然后我们执行delattr,删除了刚才设置的成员属性size,这次再打印来看,报错了。

dir()这个函数可以获取当前对象所有可以访问的成员的列表。正好,让我们来看看是否从还存在从B类中继承的成员属性name

 res = dir(obj)
 print(res)
 ​
 ---
 ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'name']

可以看到打印结果的最后面,确实还存在着name这个成员属性。

以上函数在讲解的过程中,我们使用的都是成员属性,而成员方法其实是一样的。因为这几个函数说针对的对象都是「成员」。

另外需要注意的一点是,以上所有这些常用函数,都是在可访问的情况下才可执行。我们还有一些不可访问的情况,比如说「私有成员属性」,这种成员是无法被访问或者操作的,我们随便拿个函数来举一个例子看看:

 class D():
     name = '张三'
     _age = 25
     __sex = 'female'
 ​
     print(f'Sex:{__sex}')
 ​
 obj = D()
 getattr(obj, '__sex')
 ​
 ---
 Sex:female
 AttributeError: 'D' object has no attribute '__sex'

可以看到,当我们意图用getattr来获取实例化对象obj中的__sex属性时报错了。无法正确访问。

 res = dir(obj)
 print(res)
 ​
 ---
 ['_D__sex', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_age', 'name']

当我们使用dir()来查看的时候,其中也并没有__sex这个成员属性,有的只是类的私有成员属性:_D__sex, 我们需要借助类从内部才可访问到:

 getattr(obj, '_D__sex')
 ​
 ---
 'female'

这样是可以的。

魔术方法

我们在这节课之前,讲到过「魔术方法」, 那我们已经了解,魔术方法是不需要手动调用就可以自动执行的方法。

那我们之前已经讲解过__init__,这是一个初始化方法。然后还有一个__del__方法,是一个销毁方法。

这两个方法除了功能上的不同之外,还有一个最大的不同点就是被触发的机制是不一样的。其实,魔术方法中,最重要的一点就是要了解方法的触发机制是什么。

让我们先列出来常用的魔术方法,包括其触发机制,作用以及参数等等...

  1. __init__, 初始化方法, *****

触发机制:当实例化对象之后就会立即触发的方法作用:为当前创建的对象完成一些初始化的操作,比如:成员属性的赋值, 方法的调用, 打开或者创建一些资源等等。参数:一个self, 接收当前对象,其他参数根据需求进行定义即可。返回值:注意事项:

  1. __new__,构造方法, ****

触发机制:实例化对象时自动触发(在__init__之前触发)作用:管理控制对象创建的过程参数:一个cls接收当前类,其它参数根据初始化方法的参数进行决定返回值:必须返回object.__new__(cls)进行对象的创建,如果没有返回值,则实例化对象的结果为None注意事项: __new__方法的参数和__init__方法的参数要保持一致,除了第一个参数。必须返回object.__new__(cls)进行对象的创建,如果没有返回值,则实例化对象的结果为None应用场景:设计模式中的单例设计模式。

  1. __del__,析构方法, *****

触发机制:当该类对象被销毁时,自动触发作用: 关闭或释放对象创建时打开或创建的一些资源参数: 一个self,接受当前的对象返回值:注意事项:

  1. __call__ , ***

触发机制: 把对象当作函数直接调用时自动触发作用: 一般用于归纳类或对象的操作步骤,方便调用参数:一个self接收当前对象,其它参数根据调用需求缺点返回值:可有可无

5.__len__

触发机制: 当使用len函数去检测当前对象的时候自动触发作用: 可以使用len函数检测当前对象中某个数据的信息参数: 一个self接收当前对象返回值:必须有,并且必须是一个整型注意事项len要获取什么属性的值,就在返回值中返回哪个属性的长度即可

6.__str__

触发机制: 当使用str或者print函数对对象进行操作时自动触发作用: 代码对象进行字符串的返回,可以自定义打印的信息参数:一个self,接收当前对象返回值:必须有,而去必须是字符串类型的值

7.__repr__

触发机制: 在使用repr方法对当前对象进行转换时自动触发作用: 可以设置repr函数操作对象的结果参数: 一个self,接收当前对象返回值: 必须有,而去必须是字符串类型的值注意:正常情况下,如果没有__str__这个魔术方法,__repr__方法就会代替__str__魔术方法

8.__bool__

触发机制: 当前使用bool函数转换当前对象时,自动触发.默认情况下,对象会转为True作用: 可以代替对象进行bool类型的转换,可以转换任何数据参数: 一个self接收对象返回值: 必须是一个布尔类型的返回值

以上,我们把常用魔术方法都列出来之后,然后我们来些代码进行讲解。让我们先创建一个Person类,然后在其中协商构造方法,初始化方法和析构方法。

 # 定义一个人
 class Person():
 ​
     # 构造方法
     def __new__(cls, *args, **kwargs):
         print(args)
         print(kwargs)
 ​
     # 初始化方法
     def __init__(self, name, age, sex):
         print('触发初始化方法:__init__')
         self.name = name
         self.age = age
         self.sex = sex
 ​
     # 析构方法
     def __del__(self):
         print('触发了析构方法:__del__')
 ​
 # 实例化对象
 zs = Person('张三丰', 210, '男')
 print(zs)
 ​
 ---
 ('张三丰', 210, '男')
 {}
 None

当我们完成实例化的时候,「构造方法」先是用*args接收了所有传递的参数,并且使用存储了元组。我们可以看到,**kwargs什么都没接收到,所以打印为空。

当「构造方法」执行完之后,也并没有去执行「初始化方法」和「析构方法」,这又是为什么呢?这是因为如果在「构造方法」中没有返回对象,这对象无法创建。要想对象进行创建,这我们必须返回object.__new__(cls)进行对象的创建。这在之前「构造方法」的说明里有说明。这个cls参数是什么呢?我们直接来看代码:

 # 定义一个人
 class Person():
 ​
     # 构造方法
     def __new__(cls, *args, **kwargs):
         print(args)
         # print(kwargs)
         print(cls)
         # 如果该方法中没有返回对象,则无法创建对象
         return object.__new__(cls)
 ​
     # 初始化方法
     def __init__(self, name, age, sex):
         print('触发初始化方法:__init__')
         self.name = name
         self.age = age
         self.sex = sex
 ​
     # 析构方法
     def __del__(self):
         print('触发了析构方法:__del__')
 ​
 # 实例化对象
 zs = Person('张三丰', 210, '男')
 print(zs)
 ​
 ---
 ('张三丰', 210, '男')
 <class '__main__.Person'>
 触发初始化方法:__init__
 <__main__.Person object at 0x107f3c070>

现在可以看到,我们打印了cls, 实际上就是Person这个类。当我们返回object.__new__(cls)之后,可以看到__init__初始化方法正确运行了,执行了方法内的打印方法。然后最后,我们打印了zs这个实例化对象。那为什么__del__析构方法没有触发?因为我们是在Jupyter中执行,并未执行释放,此时我们如果del zs,则会触发析构方法,或者,我们讲上述代码保存为一个22.py文件,然后单独执行,这个时候Python的垃圾回收机制会执行,就会进行释放,从而触发析构方法。如下图:

raw-image

我们接着上面写的代码在22.py中继续写:

 zs()
 ​
 ---
 TypeError: 'Person' object is not callable

报警,告知我们这个类当中没有cllable。那如果我们讲这个类改造下,加上__call__:

 # 魔术方法
 # 定义一个人
 class Person():
     # 构造方法
     def __new__(cls, *args, **kwargs):
         ...
     # 初始化方法
     def __init__(self, name, age, sex):
         ...
     def __call__(slef, *args, **kwargs):
         print('你把对象当成了函数进行调用。')
     # 析构方法
     def __del__(self):
         ...
 ​
 # 实例化对象
 ...
 zs()
 ​
 ---
 ...
 你把对象当成了函数进行调用。
 触发了析构方法:__del__

这样,我们直接执行zs()就没问题了,可以把对象当作函数直接调用时自动触发。

让我们继续,返回到22.ipynb笔记本文件中,让我们重新定义一个类:

 class Demo():
     items = []
 ​
 # 实例化对象
 obj = Demo()
 len(obj)
 ​
 ---
 TypeError: object of type 'Demo' has no len()

报错信息中可以看出,这个实例化对象是没有len()方法的。我们如果给它加上__len__之后就会让其拥有len()方法。

 class Demo():
     items = []
 ​
     def __len__(self):
         return len(self.items)
 # 实例化对象
 obj = Demo()
 print(len(obj))
 ​
 ---
 0

因为当前我们在类中定义的items里面没有数据,所以返回的长度必然也是0。但是这个返回值是必须要有的,需要返回一个整型才行。

来,我们看看如果这个方法的返回值写死会如何:

 class Demo():
     items = []
 ​
     def __len__(self):
         return 1
 # 实例化对象
 obj = Demo()
 obj.items = [1, 2, 3, 4, 5, 6, 7]
 print(len(obj))
 ​
 ---
 1

很明显,我们重新给obj.items进行了赋值,目前其长度是7, 可是返回值依然是1。说明__len__的返回值只要四个整型就行。那我们就需要注意了,len需要获取什么属性的值,就在返回值中返回哪个属性的长度即可。当我们用正确的方式返回的时候就会是这样:

 class Demo():
     items = []
 ​
     def __len__(self):
         return len(self.items)
 # 实例化对象
 obj = Demo()
 obj.items = [1, 2, 3, 4, 5, 6, 7]
 print(len(obj))
 ​
 ---
 7

让我们继续接着这段代码来玩:

 ...
 res = str(obj)
 print(res)
 ​
 ---
 <__main__.Demo object at 0x110631270>

发现没有,虽然我们使用了str()方法,可是最后返回的结果,和直接打印obj的结果是一样的。那为什么会这样呢?这是因为我们这个当前的方法其实是对obj对象进行了一个转化字符串操作,而其本身就返回了一个<__main__.Demo object at 0x110631270>的字符串。

其实这个返回的字符串我们也是可以自定义的, 使用__str__方法给一个返回值就可以了。

 class Demo():
     items = []
     ...
     def __str__(self):
         return '<__Demo__, 此字符串返回的是茶桁自定义的结果。>'
 # 实例化对象
 obj = Demo()
 ...
 print(obj)
 ​
 ---
 <__Demo__, 此字符串返回的是茶桁自定义的结果。>

我们直接打印了obj对象,因为__str__方法的存在,所以现在直接打印了返回的字符串。也就是说,该方法可以代替对象进行str或者print的字符串信息返回。

继续来看:

 class Demo():
 ​
     # def __str__(self):
         # ...
     
     def __repr__(self):
         return '这是一个repr返回的内容'
 ​
 # 实例化对象
 obj = Demo()
 print(obj)
 ​
 ---
 这是一个repr返回的内容

可以看到我在类中注释了__str__方法,这是因为只有其不存在的情况下,__repr__ 方法才会起作用,可以替代__str__方法。

那么到底__str____repr__两个到底有什么区别呢?让我们直接在代码里找答案:

 num = 521
 print(str(num))
 print(repr(num))
 ​
 ---
 521
 521

这个时候两个的结果都是一样的,似乎并看不出两者到底有什么区别。别急,让我们继续往后做这个实验:

 num = 521
 r1 = str(num)
 r2 = repr(num)
 print(f'r1: {r1}, {type(r1)}')
 print(f'r2: {r2}, {type(r2)}')
 ​
 ---
 r1: 521, <class 'str'>
 r2: 521, <class 'str'>

两者的类型都是一样的,返回了一个字符串类。难道这两者就正的毫无区别吗?Python得创建者吃饱了撑的没事做两个功能一模一样但是名字不同的方法?

 s = '521'
 r1 = str(s)
 r2 = repr(s)
 print(f'r1: {r1}, {type(r1)}')
 print(f'r2: {r2}, {type(r2)}')
 ​
 ---
 r1: 521, <class 'str'>
 r2: '521', <class 'str'>

仔细看,两者似乎有了细微的差别。repr解析的结果带着引号。

那么,strrepr函数都可以把其他类型的数据转为字符串类型。str函数会把对象转为更适合人阅读的形式,repr函数会把对象转为解释器读取的形式。

如果数据对象并没有更明显的区别的话,strrepr的转化结果还真没什么区别。

这两者的区别,其实只要了解一下就可以了。大部分时候,并不需要那么较真。

接着让我继续来看看__bool__

 res = bool(obj)
 print(res)
 ​
 ---
 True

当我们对obj使用bool()方法的时候,返回值为True。 那说明其中包含了一个bool机制,并且默认返回值为True

这个时候让我们来定义一下看看:

 class Demo():
     items = []
     ...
     def __bool__(self):
         return bool(self.items)
 ​
 # 实例化对象
 obj = Demo()
 res = bool(obj)
 print(res)
 ​
 ---
 False

由于我们的items中设置为空值,而我们将前面定义objitems的那段代码删掉了,所以这个时候,传入方法的self.items的值也为空,必然返回值就是False。也证明了,bool(obj)拿到的返回值就是类里定义的的__bool__中返回的对象。

介绍完常用的一些魔术方法之后,我们再来看一些其他的魔术方法。同样是魔术方法,为什么我要明显的区别开来讲呢?那是因为现在开始说讲的魔术方法都是针对成员的,是一些成员相关魔术方法。


  1. __getattribute__: 优先级最高

触发机制: 当访问对象成员时,自动触发,无论当前成员是否存在作用: 可以在获取对象成员时,对数据进行一些处理参数: 1. self接收对象,2. item接收当前访问的成员名称返回值: 可有可无,返回的值就是访问的结果注意事项: 在当前的魔术方法中,禁止对当前对象的成员进行访问,会触发递归。如果想要在当前魔术方法中访问对象的成员必须使用object来进行访问。格式: object.__getattribute__(self,item)


  1. __getattr__

触发机制:当访问对象中不存在的成员时,自动触发作用:防止访问不存在的成员时报错,也可以为不存在的成员进行赋值操作参数: 1. self接收当前对象,2. item接收当前访问的成员名称返回值:可有可无注意事项:当存在 getattribute 方法时,会去执行 getattribute 方法。也要注意,不要在当前的方法中再次去访问这个不存在的成员,会触发递归操作


  1. __setattr__

触发机制: 当给对象的成员进行赋值操作时会自动触发(包括添加,修改)作用: 可以限制或管理对象成员的添加和修改操作参数: 1. self接收当前对象 2. key设置的成员名 3. val设置的成员值返回值: 无注意事项:在当前的魔术方法中禁止给当前对象的成员直接进行赋值操作,会触发递归操作。如果想要给当前对象的成员进行赋值,需要借助 object格式object.__setattr__(self,key,value)


  1. __delattr__

触发机制: 当删除对象成员时自动触发作用: 可以去限制对象成员的删除,还可以删除不存在成员时防止报错参数:1. self接收当前对象 2. item删除的成员名称返回值: 无注意事项: 在当前魔术方法中禁止直接删除对象的成员,会触发递归操作。如果想要删除当前对象的成员,那么需要借助 object格式object.__delattr__(self,item)

好了,按照惯例,让我们上代码, 先让我们来定义一个最正常的类,并且实例化它:

 class Person():
     name = 'name'
     age = 0
     sex = 'male'
 ​
     def __init__(self, name, age, sex):
         self.name = name
         self.age = age
         self.sex = sex
 ​
     def say(self):
         print('say something...')
 ​
     def sing(self):
         print('sing a song...')
 ​
 # 实例化对象
 obj = Person('张三丰', 280, '男')
 print(obj.name)
 ​
 ---
 张三丰

这个时候,让我们在类中定义一个方法:__getattrbute__()

 class Person():
     ...
     def __getattribute__(self, item):
         pass
 ​
 # 实例化对象
 obj = Person('张三丰', 280, '男')
 print(obj.name)
 ​
 ---
 None

可以看到,虽然我们在实例化对象的时候传入了成员值,但是当我们打印的时候返回值为None。如果我们这个时候修改一下这个魔术方法:

 class Person():
     ...
     def __getattribute__(self, item):
         return 'abc'
 ​
 ...
 print(obj.name)
 print(obj.sex)
 ---
 abc
 abc

那我们拿到的就是得到的内容。不仅是name, 任意我们传入的成员,返回的值都为__getattribute__返回的值。当获取对象成员时,这个方法进行处罚,其中的item形参就是我们想要获取的成员属性。第一次是obj.name, 第二次是obj.sex,但是无论你调用的是什么成员,拿到的都是这个方法的返回值abc

那既然这样,是不是我们返回对象的成员属性就可以了?

 class Person():
     ...
     def __getattribute__(self, item):
         return self.name
 ​

千万不要这么做,这样会引起方法的无限递归调用,最终导致栈溢出。那么是不是我们就没办法了?也不是,我们需要使用object.__getattribute__(self, item)

 class Person():
     ...
     
     # 获取对象成员的时候触发
     def __getattribute__(self, item):
         return object.__getattribute__(self, item)
 ​
 # 实例化对象
 obj = Person('张三丰', 280, '男')
 print(obj.name)
 print(obj.sex)
 ​
 ---
 张三丰
 男

这样,我们就获取到了正确的返回值。我们这里讲解一个__getattribute__方法,限于篇幅的原因,我们其他的几个方法就不细致讲了。在我们先前的列表内,每一个方法的触发机制,作用,参数和注意事项我们都有写清楚。大家可以执行去看看,并做一些测试。让我们赶紧进入下一个阶段,不过在这之前呢,我们还是需要讲访问成员的顺序给大家强调一下,这个还是比较重要:

  1. 调用 __getattribute__魔术方法
  2. 调用数据描述符
  3. 调用当前对象的成员
  4. 调用当前类的成员
  5. 调用非数据描述符
  6. 调用父类的成员
  7. 调用__getattr__魔术方法

以上步骤是调用某个成员时的顺序,前面的能够调用成功,后面则不再执行。至于描述符,咱们下节课来详细讲。

好了,本节课到这里就结束了,让我们先预告一下,下节课呢,我们来讲讲面向对象中的「描述符和设计模式」。大家期待一下吧。

记得课后好好做练习,目前我们的课程稍微有些难度了,只有保持一定的练习量,才能理解并记住。

小伙伴们,下节课再见了。下课。


9會員
62內容數
从基础开始,再到Python,然后是CV、BI、NLP等相关技术。从头到尾详细的教授一边人工智能。
留言0
查看全部
發表第一個留言支持創作者!
茶桁的沙龍 的其他內容
Hi,大家好。我是茶桁。 今天开始,我们要迈向Python的另外一个台阶了,那就是面向对象。 面向对象编程(Object Oriented Programming),简称为OOP,是一种以对象为中心的程序设计思想。 与之相对的,就是面向过程编程(Procedure Oriented Pro
Hi,大家好。我是茶桁。 在我们日常使用Python或者其他编程语言的时候,不可避免的都会出现报错和异常。那么,我们今天就来谈谈异常。 什么是异常? 异常异常,根据名字简单理解,那就是非正常,也就是没有达到预期目标。
Hi, 大家好。我是茶桁。 在我们之前的课程中,讲解了数据,函数,类,模块以及包。这些基本上已经构成了Python的全部了。 那么,我们在学习Python的包之后,有没有思考过,既然Python有内置模块,我们也可以自己写一些模块来使用,那一定有很多第三方写过相应的模块来供我们使用。那么,这
Hi, 大家好。我是茶桁。 这一段Python之旅怎么样?还算顺利吧? 之前我们都学习了些什么?有基本常识,流程,函数,不同类型的数据以及一些模块对吧?并且还做了一些练习来巩固所学过的内容。 那么今天,我们接着来学习模块。不过今天要学的模块和以往不太一样了,以前我们学习的都是Python内
Hi,大家好。我是茶桁。 不知不觉中,咱们针对人工智能的Python课程已经过去了一半。相信大家这段时间也都有所进步了。 今天这节课呢,我给大家划一个重点。不仅仅是Python,很多语言里都是通用的,而且非常的强大。这就是我们的正则表达式。 说起正则表达式,很多程序员其实对其都不是很重视,但是
Hi, 大家好。我是茶桁。 上一节课最后,我让我家去预习一下日历和时间的相关模块,不知道大家有没有去预习。不管如何,这节课,让我们开始做一个练习:万年历。 没有预习的小伙伴也跟着一起,在本次练习完成的时候,相信你会对这些模块有了初步的了解。 好,让我们开始吧。 首先,我们需要来看看calen
Hi,大家好。我是茶桁。 今天开始,我们要迈向Python的另外一个台阶了,那就是面向对象。 面向对象编程(Object Oriented Programming),简称为OOP,是一种以对象为中心的程序设计思想。 与之相对的,就是面向过程编程(Procedure Oriented Pro
Hi,大家好。我是茶桁。 在我们日常使用Python或者其他编程语言的时候,不可避免的都会出现报错和异常。那么,我们今天就来谈谈异常。 什么是异常? 异常异常,根据名字简单理解,那就是非正常,也就是没有达到预期目标。
Hi, 大家好。我是茶桁。 在我们之前的课程中,讲解了数据,函数,类,模块以及包。这些基本上已经构成了Python的全部了。 那么,我们在学习Python的包之后,有没有思考过,既然Python有内置模块,我们也可以自己写一些模块来使用,那一定有很多第三方写过相应的模块来供我们使用。那么,这
Hi, 大家好。我是茶桁。 这一段Python之旅怎么样?还算顺利吧? 之前我们都学习了些什么?有基本常识,流程,函数,不同类型的数据以及一些模块对吧?并且还做了一些练习来巩固所学过的内容。 那么今天,我们接着来学习模块。不过今天要学的模块和以往不太一样了,以前我们学习的都是Python内
Hi,大家好。我是茶桁。 不知不觉中,咱们针对人工智能的Python课程已经过去了一半。相信大家这段时间也都有所进步了。 今天这节课呢,我给大家划一个重点。不仅仅是Python,很多语言里都是通用的,而且非常的强大。这就是我们的正则表达式。 说起正则表达式,很多程序员其实对其都不是很重视,但是
Hi, 大家好。我是茶桁。 上一节课最后,我让我家去预习一下日历和时间的相关模块,不知道大家有没有去预习。不管如何,这节课,让我们开始做一个练习:万年历。 没有预习的小伙伴也跟着一起,在本次练习完成的时候,相信你会对这些模块有了初步的了解。 好,让我们开始吧。 首先,我们需要来看看calen
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
美國總統大選只剩下三天, 我們觀察一整週民調與金融市場的變化(包含賭局), 到本週五下午3:00前為止, 誰是美國總統幾乎大概可以猜到60-70%的機率, 本篇文章就是以大選結局為主軸來討論近期甚至到未來四年美股可能的改變
Thumbnail
Faker昨天真的太扯了,中國主播王多多點評的話更是精妙,分享給各位 王多多的點評 「Faker是我們的處境,他是LPL永遠繞不開的一個人和話題,所以我們特別渴望在決賽跟他相遇,去直面我們的處境。 我們曾經稱他為最高的山,最長的河,以為山海就是盡頭,可是Faker用他28歲的年齡...
Thumbnail
2/22 #盤後分析 👉👉👉 2. 那老豬認爲傳產股等都比較可惜,昨天好不容易亮出一根出來了,也是算整理夠久了,卻遇到今天的殺盤,不知道這樣一下,股價又要這樣沈澱下去多久。 以上內容為心得分享,無買賣建議推廣,禁止盲目跟單者
Thumbnail
喬: 彷彿才見面,忍不住的歡欣雀躍,怎麼一眨眼,我又帶著這滿身的相思回到府城了呢?北上之前的等待是那麼漫長難耐,而相聚的時刻卻又如此短暫易逝,為什麼? 我就這麼整日耽溺在等待與想念之中而無法自拔呀! 那天離開臺北,不敢打電話給你,因為怕聽見你低沉的聲音,我會哭泣,又會捨不得走。
Thumbnail
大腸鏡檢查一直是探查大腸管腔內情形最好的方法。以前很多人因為怕痛、對檢查不了解,或是聽人說大腸鏡檢查很辛苦而不願意受檢。如今,因麻醉技術已相當進步,大家多了「無痛大腸鏡」這項選擇,大可舒服接受檢查。而且,只要檢查前做好準備,也就是清腸,就能大大提升檢查準確率,過程也會更加順利喔! 完成高品質大腸鏡檢
Facebook 跳出我兩年前的今天動態回顧,拿來複製貼上可以混一篇文章。回頭再看覺得有點文法或是詞彙上的小錯誤,但也不必改,保留原狀更好。 這篇動態應該是那陣子的感觸,現在回頭看,好像也還是這麼想。
Thumbnail
在一整個月的工作坊+教練指導下來,學員們有非常多的收穫,但也感受到有需要更突破的地方,因此除了基礎班:走出風暴工作坊之外,我加開了一場突破班:創造心生命工作坊,沒有參加過基礎班的也還是可以參加創造心生命工作坊,但建議還是要回來補走出風暴工作坊,因為在走出風暴工作坊之中,會有清楚的對外遇事件發生機制的
Thumbnail
週六22:00-00:00 首播、週日15:00-17:00重播 (CST)。FM98.1 News98電台。大台北地區以外請利用線上收聽 www.news98.com.tw 點選「線上收聽」鏈結即可。亦可利用 hiChannel 收聽。
Thumbnail
The Beatles第一次登上Ed Sullivan Show電視現場,1964年2月9日。週六22:00-00:00 播出 (CST)。FM98.1 News98電台。大台北地區以外請利用線上收聽 www.news98.com.tw 點選「線上收聽」鏈結即可。
Thumbnail
週六22:00-00:00 播出 (CST)。FM98.1 News98電台。大台北地區以外請利用線上收聽 www.news98.com.tw 點選「線上收聽」鏈結即可。亦可利用 hiChannel 收聽。
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
美國總統大選只剩下三天, 我們觀察一整週民調與金融市場的變化(包含賭局), 到本週五下午3:00前為止, 誰是美國總統幾乎大概可以猜到60-70%的機率, 本篇文章就是以大選結局為主軸來討論近期甚至到未來四年美股可能的改變
Thumbnail
Faker昨天真的太扯了,中國主播王多多點評的話更是精妙,分享給各位 王多多的點評 「Faker是我們的處境,他是LPL永遠繞不開的一個人和話題,所以我們特別渴望在決賽跟他相遇,去直面我們的處境。 我們曾經稱他為最高的山,最長的河,以為山海就是盡頭,可是Faker用他28歲的年齡...
Thumbnail
2/22 #盤後分析 👉👉👉 2. 那老豬認爲傳產股等都比較可惜,昨天好不容易亮出一根出來了,也是算整理夠久了,卻遇到今天的殺盤,不知道這樣一下,股價又要這樣沈澱下去多久。 以上內容為心得分享,無買賣建議推廣,禁止盲目跟單者
Thumbnail
喬: 彷彿才見面,忍不住的歡欣雀躍,怎麼一眨眼,我又帶著這滿身的相思回到府城了呢?北上之前的等待是那麼漫長難耐,而相聚的時刻卻又如此短暫易逝,為什麼? 我就這麼整日耽溺在等待與想念之中而無法自拔呀! 那天離開臺北,不敢打電話給你,因為怕聽見你低沉的聲音,我會哭泣,又會捨不得走。
Thumbnail
大腸鏡檢查一直是探查大腸管腔內情形最好的方法。以前很多人因為怕痛、對檢查不了解,或是聽人說大腸鏡檢查很辛苦而不願意受檢。如今,因麻醉技術已相當進步,大家多了「無痛大腸鏡」這項選擇,大可舒服接受檢查。而且,只要檢查前做好準備,也就是清腸,就能大大提升檢查準確率,過程也會更加順利喔! 完成高品質大腸鏡檢
Facebook 跳出我兩年前的今天動態回顧,拿來複製貼上可以混一篇文章。回頭再看覺得有點文法或是詞彙上的小錯誤,但也不必改,保留原狀更好。 這篇動態應該是那陣子的感觸,現在回頭看,好像也還是這麼想。
Thumbnail
在一整個月的工作坊+教練指導下來,學員們有非常多的收穫,但也感受到有需要更突破的地方,因此除了基礎班:走出風暴工作坊之外,我加開了一場突破班:創造心生命工作坊,沒有參加過基礎班的也還是可以參加創造心生命工作坊,但建議還是要回來補走出風暴工作坊,因為在走出風暴工作坊之中,會有清楚的對外遇事件發生機制的
Thumbnail
週六22:00-00:00 首播、週日15:00-17:00重播 (CST)。FM98.1 News98電台。大台北地區以外請利用線上收聽 www.news98.com.tw 點選「線上收聽」鏈結即可。亦可利用 hiChannel 收聽。
Thumbnail
The Beatles第一次登上Ed Sullivan Show電視現場,1964年2月9日。週六22:00-00:00 播出 (CST)。FM98.1 News98電台。大台北地區以外請利用線上收聽 www.news98.com.tw 點選「線上收聽」鏈結即可。
Thumbnail
週六22:00-00:00 播出 (CST)。FM98.1 News98電台。大台北地區以外請利用線上收聽 www.news98.com.tw 點選「線上收聽」鏈結即可。亦可利用 hiChannel 收聽。