Python基础入门笔记:面向对象
面向对象
Python 对属性的访问控制是靠程序员自觉的。
我们也可以把方法看成是类的属性的,那么方法的访问控制也是跟属性是一样的,也是没有实质上的私有方法。一切都是靠程序员自觉遵守 Python 的编程规范。
3.1 类
3.1.1 方法的装饰器
@classmethod
:调用的时候直接使用类名类调用,而不是某个对象@property
:可以像访问属性一样调用方法
class UserInfo:
...
@classmethod
def get_name(cls):
return cls.lv
@property
def get_age(self):
return self._age
if __name__ == '__main__':
...
# 直接使用类名类调用,而不是某个对象
print(UserInfo.lv)
# 像访问属性一样调用方法(注意看get_age是没有括号的)
print(userInfo.get_age)
复制代码
3.1.2 继承
语法格式:
class ClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
复制代码
当然上面的是单继承,Python 也是支持多继承的(注意: Java 是单继承、多实现),具体的语法如下:
class ClassName(Base1,Base2,Base3):
<statement-1>
.
.
.
<statement-N>
复制代码
多继承有一点需要注意的:若是父类中有相同的方法名,而在子类使用时未指定,Python 在圆括号中父类的顺序,从左至右搜索 , 即方法在子类中未找到时,从左到右查找父类中是否包含方法。
继承的子类的好处:
- 会继承父类的属性和方法
- 可以自己定义,覆盖父类的属性和方法
3.1.3 多态
看个例子就好了:
class User(object):
def __init__(self, name):
self.name = name
def printUser(self):
print('Hello !' + self.name)
class UserVip(User):
def printUser(self):
print('Hello ! 尊敬的Vip用户:' + self.name)
class UserGeneral(User):
def printUser(self):
print('Hello ! 尊敬的用户:' + self.name)
def printUserInfo(user):
user.printUser()
if __name__ == '__main__':
userVip = UserVip('大金主')
printUserInfo(userVip)
userGeneral = UserGeneral('水货')
printUserInfo(userGeneral)
复制代码
输出结果:
Hello ! 尊敬的Vip用户:大金主
Hello ! 尊敬的用户:水货
复制代码
可以看到,userVip 和 userGeneral 是两个不同的对象,对它们调用 printUserInfo 方法,它们会自动调用实际类型的 printUser 方法,作出不同的响应。这就是多态的魅力。
PS:有了继承,才有了多态,也会有不同类的对象对同一消息会作出不同的相应。
3.1.4 Python中的魔法方法
在 Python 中,所有以 "**" 双下划线包起来的方法,都统称为"魔术方法"。比如我们接触最多的 init__
。魔术方法有什么作用呢?
使用这些魔术方法,我们可以构造出优美的代码,将复杂的逻辑封装成简单的方法。
我们可以使用 Python 内置的方法 dir()
来列出类中所有的魔术方法。示例如下:
class User(object):
pass
if __name__ == '__main__':
print(dir(User()))
复制代码
输出的结果:
可以看到,一个类的魔术方法还是挺多的,截图没有截全。不过我们只需要了解一些常见和常用的魔术方法就好了。
1、属性的访问控制
Python 没有真正意义上的私有属性。然后这就导致了对 Python 类的封装性比较差。我们有时候会希望 Python 能够定义私有属性,然后提供公共可访问的 get 方法和 set 方法。Python 其实可以通过魔术方法来实现封装。
方法 | 说明 |
---|---|
__getattr__(self, name) |
该方法定义了你试图访问一个不存在的属性时的行为。因此,重载该方法可以实现捕获错误拼写然后进行重定向,或者对一些废弃的属性进行警告。 |
__setattr__(self, name, value) |
定义了对属性进行赋值和修改操作时的行为。不管对象的某个属性是否存在,都允许为该属性进行赋值。有一点需要注意,实现 __setattr__ 时要避免"无限递归"的错误 |
__delattr__(self, name) |
__delattr__ 与 __setattr__ 很像,只是它定义的是你删除属性时的行为。实现 __delattr__ 是同时要避免"无限递归"的错误 |
__getattribute__(self, name) |
__getattribute__ 定义了你的属性被访问时的行为,相比较,__getattr__ 只有该属性不存在时才会起作用。因此,在支持 __getattribute__ 的 Python 版本,调用__getattr__ 前必定会调用 __getattribute__ ,__getattribute__ 同样要避免"无限递归"的错误。 |
2、对象的描述器
一般来说,一个描述器是一个有“绑定行为”的对象属性 (object attribute),它的访问控制被描述器协议方法重写。这些方法是 __get__()
,__set__()
和 __delete__()
。有这些方法的对象叫做描述器。
默认对属性的访问控制是从对象的字典里面 (__dict__)
中获取 (get) , 设置 (set) 和删除 (delete) 。举例来说, a.x
的查找顺序是 a.__dict__['x']
,然后 type(a).__dict__['x']
,然后找 type(a)
的父类 ( 不包括元类 (metaclass) )。如果查找到的值是一个描述器,Python 就会调用描述器的方法来重写默认的控制行为。这个重写发生在这个查找环节的哪里取决于定义了哪个描述器方法。注意,只有在新式类中时描述器才会起作用。
至于新式类最大的特点就是所有类都继承自 type 或者 object 的类。
在面向对象编程时,如果一个类的属性有相互依赖的关系时,使用描述器来编写代码可以很巧妙的组织逻辑。在 Django 的 ORM 中,models.Model
中的 InterField 等字段,就是通过描述器来实现功能的。
看一个例子:
class User(object):
def __init__(self, name='小明', sex='男'):
self.sex = sex
self.name = name
def __get__(self, obj, objtype):
print('获取 name 值')
return self.name
def __set__(self, obj, val):
print('设置 name 值')
self.name = val
class MyClass(object):
x = User('小明', '男')
y = 5
if __name__ == '__main__':
m = MyClass()
print(m.x)
print('\n')
m.x = '大明'
print(m.x)
print('\n')
print(m.x)
print('\n')
print(m.y)
复制代码
输出结果:
获取 name 值
小明
设置 name 值
获取 name 值
大明
获取 name 值
大明
5
复制代码
3、自定义容器(Container)
我们知道在 Python 中,常见的容器类型有:dict、tuple、list、string。其中也提到过可容器和不可变容器的概念。其中 tuple、string 是不可变容器,dict、list 是可变容器。
可变容器和不可变容器的区别在于,不可变容器一旦赋值后,不可对其中的某个元素进行修改。
那么这里先提出一个问题,这些数据结构就够我们开发使用吗?不够的时候,或者说有些特殊的需求不能单单只使用这些基本的容器解决的时候,该怎么办呢?
这个时候就需要自定义容器了,那么具体我们该怎么做呢?
功能 | 说明 |
---|---|
自定义不可变容器类型 | 需要定义 __len__ 和 __getitem__ 方法 |
自定义可变类型容器 | 在不可变容器类型的基础上增加定义 __setitem__ 和 __delitem__ |
自定义的数据类型需要迭代 | 需定义 __iter__ |
返回自定义容器的长度 | 需实现 __len__(self) |
自定义容器可以调用 self[key] ,如果 key 类型错误,抛出 TypeError,如果没法返回 key对应的数值时,该方法应该抛出 ValueError |
需要实现 __getitem__(self, key) |
当执行 self[key] = value 时 |
调用是 __setitem__(self, key, value) 这个方法 |
当执行 del self[key] 方法 |
其实调用的方法是 __delitem__(self, key) |
当你想你的容器可以执行 for x in container: 或者使用 iter(container) 时 |
需要实现 __iter__(self) ,该方法返回的是一个迭代器 |
还有很多魔术方法,比如运算符相关的模式方法,就不在该文展开了。
3.2 枚举类
3.2.1 什么是枚举
举例,直接看代码:
from enum import Enum
Month = Enum('Month1', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
# 遍历枚举类型
for name, member in Month.__members__.items():
print(name, '---------', member, '----------', member.value)
# 直接引用一个常量
print('\n', Month.Jan)
复制代码
输出结果:
Jan --------- Month1.Jan ---------- 1
Feb --------- Month1.Feb ---------- 2
Mar --------- Month1.Mar ---------- 3
Apr --------- Month1.Apr ---------- 4
May --------- Month1.May ---------- 5
Jun --------- Month1.Jun ---------- 6
Jul --------- Month1.Jul ---------- 7
Aug --------- Month1.Aug ---------- 8
Sep --------- Month1.Sep ---------- 9
Oct --------- Month1.Oct ---------- 10
Nov --------- Month1.Nov ---------- 11
Dec --------- Month1.Dec ---------- 12
Month.Jan
复制代码
可见,我们可以直接使用 Enum 来定义一个枚举类。上面的代码,我们创建了一个有关月份的枚举类型 Month,这里要注意的是构造参数,第一个参数 Month 表示的是该枚举类的类名,第二个 tuple 参数,表示的是枚举类的值; 当然,枚举类通过 __members__
遍历它的所有成员的方法。
注意的一点是 , member.value 是自动赋给成员的 int 类型的常量,默认是从 1 开始的。而且 Enum 的成员均为单例(Singleton),并且不可实例化,不可更改。
3.2.2 自定义枚举类型
有时候我们需要控制枚举的类型,那么我们可以 Enum 派生出自定义类来满足这种需要。修改上面的例子:
from enum import Enum, unique
Enum('Month1', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
# @unique 装饰器可以帮助我们检查保证没有重复值
@unique
class Month1(Enum):
Jan = 'January'
Feb = 'February'
Mar = 'March'
Apr = 'April'
May = 'May'
Jun = 'June'
Jul = 'July'
Aug = 'August'
Sep = 'September '
Oct = 'October'
Nov = 'November'
Dec = 'December'
if __name__ == '__main__':
print(Month1.Jan, '----------',
Month1.Jan.name, '----------', Month1.Jan.value)
for name, member in Month1.__members__.items():
print(name, '----------', member, '----------', member.value)
复制代码
输出结果:
Month1.Jan ---------- Jan ---------- January
Jan ---------- Month1.Jan ---------- January
Feb ---------- Month1.Feb ---------- February
Mar ---------- Month1.Mar ---------- March
Apr ---------- Month1.Apr ---------- April
May ---------- Month1.May ---------- May
Jun ---------- Month1.Jun ---------- June
Jul ---------- Month1.Jul ---------- July
Aug ---------- Month1.Aug ---------- August
Sep ---------- Month1.Sep ---------- September
Oct ---------- Month1.Oct ---------- October
Nov ---------- Month1.Nov ---------- November
Dec ---------- Month1.Dec ---------- December
复制代码
4.2.3 枚举类的比较
因为枚举成员不是有序的,所以它们只支持通过标识(identity) 和相等性 (equality) 进行比较。下面来看看 ==
和 is
的使用:
from enum import Enum
class User(Enum):
Twowater = 98
Liangdianshui = 30
Tom = 12
Twowater = User.Twowater
Liangdianshui = User.Liangdianshui
print(Twowater == Liangdianshui, Twowater == User.Twowater)
print(Twowater is Liangdianshui, Twowater is User.Twowater)
try:
print('\n'.join(' ' + s.name for s in sorted(User)))
except TypeError as err:
print(' Error : {}'.format(err))
复制代码
输出结果:
False True
False True
Error : '<' not supported between instances of 'User' and 'User'
复制代码
可以看看最后的输出结果,报了个异常,那是因为大于和小于比较运算符引发 TypeError 异常。也就是 Enum 类的枚举是不支持大小运算符的比较的。
但是使用 IntEnum 类进行枚举,就支持比较功能。
import enum
class User(enum.IntEnum):
Twowater = 98
Liangdianshui = 30
Tom = 12
try:
print('\n'.join(s.name for s in sorted(User)))
except TypeError as err:
print(' Error : {}'.format(err))
复制代码
输出结果:
Tom
Liangdianshui
Twowater
复制代码
通过输出的结果可以看到,枚举类的成员通过其值得大小进行了排序。也就是说可以进行大小的比较。
3.3 元类
3.3.1 Python 中类也是对象
在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段。在 Python 中这一点也是一样的。但是,Python 中的类有一点跟大多数的编程语言不同,在 Python 中,可以把类理解成也是一种对象。对的,这里没有写错,就是对象。
因为只要使用关键字 class
,Python 解释器在执行的时候就会创建一个对象。如:
class ObjectCreator(object):
pass
复制代码
当程序运行这段代码的时候,就会在内存中创建一个对象,名字就是ObjectCreator。这个对象(类)自身拥有创建对象(类实例)的能力,而这就是为什么它是一个类的原因。
3.3.2 使用type()动态创建类
因为类也是对象,所以我们可以在程序运行的时候创建类。Python 是动态语言。动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。在之前,我们先了了解下 type()
函数。
class Hello(object):
def hello(self, name='Py'):
print('Hello,', name)
复制代码
然后再另外一个模块引用 hello 模块,输出相应信息。(其中 type()
函数的作用是可以查看一个类型和变量的类型。)
from com.strivebo.hello import Hello
h = Hello()
h.hello()
print(type(Hello))
print(type(h))
复制代码
输出信息:
Hello, Py
<class 'type'>
<class 'com.twowater.hello.Hello'>
复制代码
上面也提到过,type()
函数可以查看一个类型或变量的类型,Hello 是一个 class ,它的类型就是 type ,而 h 是一个实例,它的类型就是 com.strivebo.hello.Hello
。前面的 com.strivebo
是我的包名,hello 模块在该包名下。
在这里还要细想一下,上面的例子中,我们使用 type()
函数查看一个类型或者变量的类型。其中查看了一个 Hello class 的类型,打印的结果是: <class 'type'>
。其实 type()
函数不仅可以返回一个对象的类型,也可以创建出新的类型。class 的定义是运行时动态创建的,而创建 class 的方法就是使用 type()
函数。比如我们可以通过 type()
函数创建出上面例子中的 Hello 类,具体看下面的代码:
def printHello(self, name='Py'):
# 定义一个打印 Hello 的函数
print('Hello,', name)
# 创建一个 Hello 类
Hello = type('Hello', (object,), dict(hello=printHello))
# 实例化 Hello 类
h = Hello()
# 调用 Hello 类的方法
h.hello()
# 查看 Hello class 的类型
print(type(Hello))
# 查看实例 h 的类型
print(type(h))
复制代码
输出结果:
Hello, Py
<class 'type'>
<class '__main__.Hello'>
复制代码
在这里,需先了解下通过 type()
函数创建 class 对象的参数说明:
- class 的名称,比如例子中的起名为
Hello
- 继承的父类集合,注意 Python 支持多重继承,如果只有一个父类,tuple 要使用单元素写法;例子中继承 object 类,因为是单元素的 tuple ,所以写成
(object,)
- class 的方法名称与函数绑定;例子中将函数
printHello
绑定在方法名hello
中
具体的模式如下:type(类名, 父类的元组(针对继承的情况,可以为空),包含属性的字典(名称和值))
好了,了解完具体的参数使用之外,我们看看输出的结果,可以看到,通过 type()
函数创建的类和直接写 class 是完全一样的,因为 Python 解释器遇到 class 定义时,仅仅是扫描一下 class 定义的语法,然后调用 type()
函数创建出 class 的。
3.3.3 什么是元类
我们创建类的时候,大多数是为了创建类的实例对象。那么元类呢?元类就是用来创建类的。也可以换个理解方式就是:元类就是类的类。
通过上面 type()
函数的介绍,我们知道可以通过 type()
函数创建类:MyClass = type('MyClass', (), {})
实际上 type() 函数是一个元类。type()
就是 Python 在背后用来创建所有类的元类。
那么现在我们也可以猜到一下为什么 type()
函数是 type 而不是 Type呢?
这可能是为了和 str 保持一致性,str 是用来创建字符串对象的类,而 int 是用来创建整数对象的类。type 就是创建类对象的类。
可以看到,上面的所有东西,也就是所有对象都是通过类来创建的,那么我们可能会好奇,__class__
的 __class__
会是什么呢?换个说法就是,创建这些类的类是什么呢?
print(age.__class__.__class__)
print(name.__class__.__class__)
print(fu.__class__.__class__)
print(mEat.__class__.__class__)
复制代码
输出结果:
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>
复制代码
可以看出,把他们类的类打印结果。发现打印出来的 class 都是 type 。
一开始也提到了,元类就是类的类。也就是元类就是负责创建类的一种东西。你也可以理解为,元类就是负责生成类的。而 type 就是内建的元类。也就是 Python 自带的元类。
3.3.4 自定义元类
连接起来就是:先定义 metaclass,就可以创建类,最后创建实例。
所以,metaclass 允许你创建类或者修改类。换句话说,你可以把类看成是 metaclass 创建出来的“实例”。
class MyObject(object):
__metaclass__ = something…
[…]
复制代码
如果是这样写的话,Python 就会用元类来创建类 MyObject。当你写下 class MyObject(object)
,但是类对象 MyObject 还没有在内存中创建。Python 会在类的定义中寻找 __metaclass__
属性,如果找到了,Python 就会用它来创建类 MyObject,如果没有找到,就会用内建的 type 函数来创建这个类。如果还不怎么理解,看下下面的流程图:
举个实例:
class Foo(Bar):
pass
复制代码
它的流程是怎样的呢?
首先判断 Foo 中是否有
__metaclass__
这个属性?如果有,Python 会在内存中通过__metaclass__
创建一个名字为 Foo 的类对象(注意,这里是类对象)。如果 Python 没有找到__metaclass__
,它会继续在 Bar(父类)中寻找__metaclass__
属性,并尝试做和前面同样的操作。如果 Python在任何父类中都找不到__metaclass__
,它就会在模块层次中去寻找__metaclass__
,并尝试做同样的操作。如果还是找不到__metaclass__
,Python 就会用内置的 type 来创建这个类对象。
其实 __metaclass__
就是定义了 class 的行为。类似于 class 定义了 instance 的行为,metaclass 则定义了 class 的行为。可以说,class 是 metaclass 的 instance。
现在,我们基本了解了 __metaclass__
属性,但是,也没讲过如何使用这个属性,或者说这个属性可以放些什么?
答案就是:可以创建一个类的东西。那么什么可以用来创建一个类呢?type,或者任何使用到 type 或者子类化 type 的东东都可以。
3.4.5 元类的作用
元类的主要目的就是为了当创建类时能够自动地改变类。通常,你会为 API 做这样的事情,你希望可以创建符合当前上下文的类。
假想一个很傻的例子,你决定在你的模块里所有的类的属性都应该是大写形式。有好几种方法可以办到,但其中一种就是通过在模块级别设定__metaclass__
。采用这种方法,这个模块中的所有类都会通过这个元类来创建,我们只需要告诉元类把所有的属性都改成大写形式就万事大吉了。
总结:Python 中的一切都是对象,它们要么是类的实例,要么是元类的实例,除了 type。type 实际上是它自己的元类,在纯 Python 环境中这可不是你能够做到的,这是通过在实现层面耍一些小手段做到的。
作者:Jaybo