Python(9)面对对象高级编程(下)

简介: Python(9)面对对象高级编程(下)

四、定制类


在看到类似于__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框架,所有的类只能动态定义,因为只有使用者才能够根据表的结构定义出对应的类
目录
相关文章
|
16天前
|
存储 人工智能 数据处理
Python:编程的艺术与科学的完美交融
Python:编程的艺术与科学的完美交融
19 1
|
2天前
|
C++ Python
Python中的类与对象
Python中的类与对象
6 1
|
3天前
|
JSON 数据格式 开发者
pip和requests在Python编程中各自扮演着不同的角色
`pip`是Python的包管理器,用于安装、升级和管理PyPI上的包;`requests`是一个HTTP库,简化了HTTP通信,支持各种HTTP请求类型及数据交互。两者在Python环境中分别负责包管理和网络请求。
15 5
|
5天前
|
存储 Python 容器
Python高级编程
Python集合包括可变的set和不可变的frozenset,用于存储无序、不重复的哈希元素。创建集合可使用{}或set(),如`my_set = {1, 2, 3, 4, 5}`。通过add()添加元素,remove()或discard()删除元素,如`my_set.remove(3)`。
|
6天前
|
测试技术 Python
Python模块化方式编程实践
Python模块化编程提升代码质量,包括:定义专注单一任务的模块;使用`import`导入模块;封装函数和类,明确命名便于重用;避免全局变量降低耦合;使用文档字符串增强可读性;为每个模块写单元测试确保正确性;重用模块作为库;定期维护更新以适应Python新版本。遵循这些实践,可提高代码可读性、重用性和可维护性。
27 2
|
12天前
|
测试技术 调度 索引
python编程中常见的问题
【4月更文挑战第23天】
32 2
|
12天前
|
网络协议 算法 网络架构
Python网络编程之udp编程、黏包以及解决方案、tcpserver
Python网络编程之udp编程、黏包以及解决方案、tcpserver
|
12天前
|
机器学习/深度学习 数据挖掘 算法框架/工具
Python:编程的艺术与魅力
Python:编程的艺术与魅力
25 3
|
13天前
|
机器学习/深度学习 数据可视化 数据挖掘
实用技巧:提高 Python 编程效率的五个方法
本文介绍了五个提高 Python 编程效率的实用技巧,包括使用虚拟环境管理依赖、掌握列表推导式、使用生成器提升性能、利用装饰器简化代码结构以及使用 Jupyter Notebook 进行交互式开发。通过掌握这些技巧,可以让你的 Python 编程更加高效。
|
13天前
|
算法 Python
Python面向对象oop编程(二)
Python面向对象oop编程(二)