四、定制类
在看到类似于__xx__这样的变量或者函数时要知道,这种函数是有特殊用途的,例如可以限制实例添加属性的__slots__变量,以及判断字符长度的__len__()函数,除了这些,Python的类还有许多这样有特殊用途的函数,可以帮助我们定义类
下面来看几个常用的定义方法:
__str__
我们先来定义一个Student类,并且打印一个实例:
>>> class Student(object): ... def __init__(self,name): ... self.name = name ... >>> print(Student('lisi')) #打印lisi实例 <__main__.Student object at 0x0000028800FF61D0>
可以看到在打印实例时,输出了一堆<__main__.Student object at 0x0000028800FF61D0>,这样不易阅读,我们可以在类中添加一个__str__()方法,从而使输出更容易阅读,例如:
>>> class Student(object): ... def __init__(self,name): ... self.name = name ... def __str__(self): ... return 'Student object (name is : %s)' % self.name ... >>> print(Student('lisi')) Student object (name is : lisi) - 通过'__str__()'方法,我们可以自定义想要输出的内容,在打印实例的时候会输出,但是只有直接使用'print'时才会输出,赋值给变量的话,则还是原来的 >>> lisi = Student('lisi') >>> lisi <__main__.Student object at 0x00000208C62F64A0>
在赋值变量后,输出的还是原来的不易阅读的输出,这是因为直接显示变量调用的不是__str__(),而是__repr__(),这两个方法的区别是==__str__()返回用户看到的字符串==,而==__repr__()返回程序开发者看到的字符串,即__repr__()是为了调试服务的==
解决上面的方法就是再定义一个__repr__()方法,但是通常可以写成__repr__ = __str__
>>> class Student(object): ... def __init__(self,name): ... self.name = name ... def __str__(self): ... return 'Student object (name is : %s)' % self.name ... __repr__ = __str__ ... >>> print(Student('lisi')) Student object (name is : lisi) >>> lisi = Student('lisi') >>> print(lisi) Student object (name is : lisi)
__iter__
如果一个类想像字典或者元组那样被用于for循环,那么就必须要添加一个__iter__()方法,该方法会返回一个迭代对象,Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出,下面来看一个案例:
- 斐波那契数列: #!/usr/bin/env python3 # -*- coding: utf-8 -*- class Fib(object): def __init__(self): self.a,self.b = 0,1 #初始化定义两属性的值 def __iter__(self): #__iter__返回实例本身 return self def __next__(self): self.a,self.b = self.b,self.a + self.b if self.a > 100: raise StopIteration() return self.a for i in Fib(): print(i) #输出 1 1 2 3 5 8 13 21 34 55 89
__getitem__
上面的Fib实例虽然能够用于for循环,但是把实例直接当作列表来用还是不行,也就是说一些类似于下标的操作是无法执行的,例如:
>>> class Fib(object): ... def __init__(self): ... self.a,self.b = 0 , 1 ... def __iter__(self): ... return self ... def __next__(self): ... self.a,self.b = self.b,self.a + self.b ... if self.a > 100: ... raise StopIteration() ... return self.a ... >>> for i in Fib(): ... print(i) ... 1 1 2 3 5 8 13 21 34 55 89 >>> Fib()[5] #报错了 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'Fib' object is not subscriptable
如果想要使创建的实例可以像列表那样使用下标的话,可以再类中添加__getitem__()方法:
>>> class Fib(object): ... def __getitem__(self,n): ... a,b = 1,1 ... for i in range(n): ... a,b = b,a + b ... return a ... >>> f = Fib() >>> Fib() <__main__.Fib object at 0x000002C6A39A79D0> >>> Fib()[0] #可以使用下标获取元素 1 >>> Fib()[5] 8 >>> f[5] 8
列表还有一种切片方法,例如:
>>> list(range(100))[96:100] [96, 97, 98, 99] >>> Fib()[5:6] Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in __getitem__ TypeError: 'slice' object cannot be interpreted as an integer >>> f[5:6] Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in __getitem__ TypeError: 'slice' object cannot be interpreted as an integer
可以看到列表的切片方法是不适用于实例的,原因是__getitem__()传入的参数可能是一个int类型的,也可能是一个slice切片对象,所以需要做判断,判断传入参数是int还是slice:
>>> class Fib(object): ... def __getitem__(self,n): ... if isinstance(n,int): ... a,b = 1, 1 ... for i in range(n): ... a,b = b,a + b ... return a ... if isinstance(n,slice): ... start = n.start #这是slice带的方法 ... stop = n.stop ... if start is None: ... start = 0 ... a,b = 1 , 1 ... L = [] ... for i in range(stop): ... if i >= start: ... L.append(a) ... a,b = b,a + b ... return L ... >>> f = Fib() >>> f[0:5] [1, 1, 2, 3, 5] >>> f[5:6] [8] >>> f[0:5:2] #但是没有对step跳数参数做处理 [1, 1, 2, 3, 5] >>> f[-1:] #也无法对负数做处理 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 15, in __getitem__ TypeError: 'NoneType' object cannot be interpreted as an integer
可以从上面的案例看出,如果想要正确实现一个__getitem__()方法,还需做很多事情,例如跳数和负数等
此外,如果把对象看做是字典,那么__getitem__()的参数也可能是一个可以作为键的对象,例如str,与之对应的是__setitem__()方法,把对象视为列表或者字典来对集合赋值,还有一个__delitem__()方法,用于删除指定元素,通过这些方法,可以使我们在创建类时表现的和Python自带的列表、元组、字典没什么区别
__getattr__
- 正常情况下,当我们调用类的方法或者属性时,如果不存在,那么就会报错,例如:
>>> class Student(object): ... def __init__(self): ... self.name = 'zhangsan' ... >>> zhangsan = Student() >>> zhangsan.name 'zhangsan' >>> zhangsan.score Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Student' object has no attribute 'score'
可以看到错误信息就是在说没有找到score这个属性,想要避免这种错误,除了手动定义一个score属性之外,在Python中还可以在类中添加一个__getattr__()方法,这个方法可以动态返回一个属性,下面来看案例:
>>> class Student(object): ... def __init__(self): ... self.name = 'zhangsan' ... def __getattr__(self,value): ... if value == 'score': ... return 98 ... >>> zhangsan = Student() >>> zhangsan.name 'zhangsan' >>> zhangsan.score 98 >>> zhangsan.abc #调用不存在的属性,并且在__getattr__方法里也没有的,就会返回None >>> if zhangsan.abc == None: ... print("abc is None!!") ... abc is None!!
我们可以发现,在添加__getattr__()方法后,创建的实例除了可以直接调用name,还可以调用不存在的属性score,这是因为在调用不存在的属性时,Python解释器会试图使用__getattr__(self,'score')来尝试获得属性,这样就可以返回score的值
并且还可以返回函数:
>>> class Student(object): ... def __getattr__(self,attr): ... if attr == 'age': ... return lambda:98 ... >>> zhangsan = Student() >>> zhangsan.age #返回的是一个函数 <function Student.__getattr__.<locals>.<lambda> at 0x00000205B6373E20> >>> zhangsan.age() #相同的调用方式需要加括号,表示调用函数 98
需要注意的是,只有在没有找到指定属性的情况下,才会调用__getattr__()方法,已经存在的属性,例如上面案例中,在类里提前定义的name属性,是不会去__getattr__()方法里去找的,会直接输出
在上面的案例可以发现,在使用__getattr__()方法后,调用除了类里、__getattr__()方法里出现的属性时,都会返回None,这是因为定义的__getattr__()方法默认返回的就是None,而返回的数据,我们可以通过raise抛出异常来修改,例如:
>>> class Student(object): ... def __getattr__(self,attr): ... if attr == 'age': ... return 98 ... raise AttributeError('Student object has no attribute %s' % attr) ... >>> zhangsan = Student() >>> zhangsan.age 98 >>> zhangsan.aaa Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 5, in __getattr__ AttributeError: Student object has no attribute aaa #调用不存在的属性会抛出异常
- 利用
__getattr__()
方法,我们可以把一个类的所有属性和方法的调用全部动态化处理,不需要任何其他手段,这种动态调用可以针对完全动态的情况做调用
__call__
- 一个对象实例可以有自己的方法和属性,当我们调用实例方法的时候,我们使用
instance.metod()
来调用,而在Python中,是可以直接在实例本身上调用的 - 在创建类时,只需要添加一个
__call__()
方法,就可以直接对实例进行调用,例如:
>>> class Student(object): ... def __init__(self,name): ... self.name = name ... def __call__(self): ... print('name is : %s' % self.name) ... >>> zhangsan = Student('zhangsan') >>> zhangsan() #可以看到把直接调用了实例,后面加了括号 name is : zhangsan
可以看到,在添加__call__()方法后,可以直接调用实例本身,然后会执行__call__()方法下的代码
__call__()方法还可以定义参数,对实例进行直接调用和对一个函数直接调用是一样的,所以完全可以把对象看作成函数,把函数看作成对象,因为两者之间其实没有什么太大的区别
如果把对象看成函数,那么函数本身其实也是可以在运行期间动态创建出来的,这是因为类的实例都是在运行期间创建出来了,这样的话,对象和函数的界限就变得模糊了
那么该如何判断一个变量是对象还是函数呢,其实在更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个Callable可调用的对象,例如函数和上面定义的带有__call__()方法的类实例
使用callable()函数可以检查一个对象是否是可调用的,如果返回True,对象仍然可能调用失败,但是如果返回的是False,那么调用绝对会失败
对于函数、方法、lambda函式、类以及有__call__()方法的类实例,callable()函数都会返回True,语法就是callable(object)
下面来看几个例子:
>>> class Student(object): ... def __init__(self,name): ... self.name = name ... def __call__(self): ... print("name is : %s " % self.name) ... >>> callable(Student) True >>> zhangsan = Student('zhangsan') >>> zhangsan() name is : zhangsan >>> callable(zhangsan) True >>> callable('aaa') False >>> callable(123) False >>> callable(None) False - 可以看到类和实例返回的都是'True',其他的字符串、数字、None返回的都是'False'
五、使用枚举类
这个小结,写的不是很全,建议可以去看其他关于枚举类的资源
- 当我们需要定义常量时,有一个办法就是使用大写变量通过整数来定义,例如:
- 月份 JAN = 1 FEB = 2 MAR = 3 ... NOV = 11 DEC = 12
这样定义的好处肯定就是简单,缺点就是类型是数字,并且仍然是变量
更好的方法就是为这样的枚举类型定义一个class类类型,然后每一个常量都是类的一个唯一实例,而在Python中,提供了Enum类来实现这个功能
- 这样我们就获得了'Month'类型的枚举类,可以看到,下面直接使用'Month.Jan'来引用一个常量 >>> from enum import Enum >>> Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')) >>> Month.Jan <Month.Jan: 1> >>> Month.Mar <Month.Mar: 3> >>> Month.Dec <Month.Dec: 12> - 还可以利用for循环枚举它的全部成员 >>> for name,number in Month.__members__.items(): ... print(name,'=>',number,',',number.value) ... Jan => Month.Jan , 1 Feb => Month.Feb , 2 Mar => Month.Mar , 3 Apr => Month.Apr , 4 May => Month.May , 5 Jun => Month.Jun , 6 Jul => Month.Jul , 7 Aug => Month.Aug , 8 Sep => Month.Sep , 9 Oct => Month.Oct , 10 Nov => Month.Nov , 11 Dec => Month.Dec , 12
- 上面的
number.value
中,value
属性是自动赋值给成员的int
常量的,默认是从1开始计算 - 如果需要更加精确的控制枚举类型,可以从
Enum
派生出自定义类
>>> from enum import Enum,unique >>> @unique #@unique装饰器可以帮助检查保证没有重复值 ... class Weekday(Enum): ... Sun = 0 ... Mon = 1 ... Tue = 2 ... Wed = 3 ... Thu = 4 ... Fri = 5 ... Sat = 6 ... >>> day_1 = Weekday.Mon >>> day_1 >>> <Weekday.Mon: 1> >>> print(day_1) >>> Weekday.Mon >>> print(Weekday.Tue) Weekday.Tue >>> print(Weekday['Tue']) Weekday.Tue >>> print(day_1 == Weekday.Mon) True >>> print(day_1 == Weekday(1)) True >>> Weekday(7) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "C:\Users\RZY\AppData\Local\Programs\Python\Python310\lib\enum.py", line 385, in __call__ return cls.__new__(cls, value) File "C:\Users\RZY\AppData\Local\Programs\Python\Python310\lib\enum.py", line 710, in __new__ raise ve_exc ValueError: 7 is not a valid Weekday >>> Weekday(6) <Weekday.Sat: 6> >>> for name,number in Weekday.__members__.items(): ... print(name, '=>', number) ... Sun => Weekday.Sun Mon => Weekday.Mon Tue => Weekday.Tue Wed => Weekday.Wed Thu => Weekday.Thu Fri => Weekday.Fri Sat => Weekday.Sat - 从上面可以看出,既可以用成员名称引用枚举常量,也可以直接根据value的值获得枚举常量。
六、使用元类
- 元类就是用来创建类的类
type()
type就是Python在背后用来创建所有类的元类。
Python中一切皆对象,包括整数、字符串、函数以及类,它们全部都是对象,而且它们都是从一个类创建而来,这个类就是type,type就是Python的内建元类,当然了,也可以创建自己的元类。
动态语言和静态语言最大的区别就是函数和类的定义,静态语言是在编译时定义的,而动态语言时运行时动态创建的,例如
- 想要定义一个'Hello'的类,可以写一个'hello.py'的模块,以下是在linux环境下 [root@centos-1 ~]# vim hello.py #!/usr/bin/env python3 # -*- coding: utf-8 -*- class Hello(object): def __init__(self,name='zhangsan'): self.name = name def hello(self): print('Hello %s' % self.name) #保存退出 [root@centos-1 ~]# python3 Python 3.9.9 (main, May 13 2022, 15:23:56) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from hello import Hello >>> zhangsan = Hello() >>> zhangsan.name 'zhangsan' >>> zhangsan.hello() Hello zhangsan >>> lisi = Hello('lisi') >>> lisi.name 'lisi' >>> lisi.hello() Hello lisi >>> type(Hello) #查看Hello类的类型,可以看到类型是type类型的 <class 'type'> >>> type(zhangsan) #查看zhangsan实例的类型,可以看到是hello.Hello,前面的hello即hello.py模块,后面的Hello即模块中的Hello类 <class 'hello.Hello'> >>> type(lisi) <class 'hello.Hello'>
在Python中,类的定义是运行时动态创建的,而创建类的方法就是使用type()函数
type()函数既可以返回一个对象的类型,也可以创建出新的类型(类),无需使用class关键字创建,例如:
>>> def fn(self,name='zhangsan'): #定义fn函数 ... print('Hello %s' % name) ... >>> Hello = type('Hello',(object,),dict(hello=fn)) #使用type创建Hello类 >>> zhangsan = Hello() >>> zhangsan.hello() Hello zhangsan >>> type(Hello) <class 'type'> >>> type(zhangsan) <class '__main__.Hello'> - 注释: type('Hello',(object,),dict(hello=fn))传入的参数分别是: 1、'Hello' : 这是创建的类的名称 2、'(object,)' : 创建的类继承的父类,如果只有一个父类,要记得'元组在只有单元素时后面要加逗号' 3、'dict(hello=fn)' : 创建的类中,方法可以和函数绑定在一起,这里把创建的'fn'函数绑定到创建类的'hello'方法上,可以看到最终实例调用的是'hello'方法,这里使用的是键值的字典格式,这段可以之间写成字典,也可以使用dict转换一下
- 可以看到,通过type创建的类和直接使用
class
关键字创建的类是一样的,这是因为Python解释器在遇到类的定义时,其实只是扫描一下类定义的语法,然后直接调用type()
函数进行创建的
扩展——metaclass
除了上面使用的type()函数动态创建类以外,要控制类的创建行为,还可以使用metaclass,metaclass直译就是元类,对于metaclass来说,廖雪峰大神的解释是这样的:
正常在定义完类之后,我们就可以根据这个类去创建出实例,步骤是:定义类——>创建实例
但是如果想创建出类呢,那么就必须根据metaclass创建出类,步骤就变成了:定义metaclass——>创建类——>创建实例,可以看做成类是metaclass创建出来的实例
metaclass是Python面向对象里最难理解的,也是最难的魔术代码,正常情况下是不会碰到需要metaclass的情况,所以这里写的是扩展,下面来简单看一下案例:
- 定义类'ListMetaclass'类,按照默认习惯,metaclass的类名总是以'Metaclass'结尾的 >>> class ListMetaclass(type): ... def __new__(cls, name, bases, attrs): ... attrs['add'] = lambda self, value: self.append(value) ... return type.__new__(cls, name, bases, attrs) ... >>> class MyList(list, metaclass=ListMetaclass): #继承父类list ... pass ... #注释: 当我们传入关键字参数'metaclass'后,魔术就生效了,它指示Python解释器在创建'MyList'类时,要通过'ListMetaclass.__new__()'来创建,我们可以修改类的定义,例如,加上新的方法然后,返回修改后的定义 其中'__new__'方法接收到的参数依次为: 1、'cls' : 当前准备创建类的对象 2、'name' : 类的名字,也就是'MyList' 3、'bases' : 类继承的父类集合,也就是'list' 4、'attrs' : 创建类的方法集合,这里写的是' attrs['add'] = lambda self, value: self.append(value) ',其中'attrs['add']'的'add'就是方法名称,后面的lambda是方法的代码,使用lambda匿名函数,实现的其实就是往实例对象里面添加元素,实例对象是一个列表 - 下面来测试一下'MyList'是否可以调用'add'方法,可以看到调用成功,但是普通的list并没有add方法 >>> L = MyList() >>> L.add(1) >>> L #可以看到,实例对象是一个列表 [1] >>> L2 = list() >>> L2.add(1) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'list' object has no attribute 'add'
七、扩展——ORM对象关系映射
这是廖雪峰大神‘面向对象高级编程’的最后一个小节,这个有点难,有兴趣可以去官网看看,这里不写具体代码了
- ORM全称Object Relational Mapping,即对象-关系映射,就是把关系型数据库的一行数据映射为一个对象,一个类对应一个表,这样写代码会更加简单,不需要直接操作SQL语句
- 要编写一个ORM框架,所有的类只能动态定义,因为只有使用者才能够根据表的结构定义出对应的类