python面向对象编程

admin 2022年1月6日01:34:07评论27 views字数 19633阅读65分26秒阅读模式

在Python中使用class关键字定义一个类,类的主体由属性(变量)和方法(函数)组成
详情可看此篇文章:https://mp.weixin.qq.com/s/jEKvWwu5MwCqLbIxmAP4iQ
面向对象编程和函数式编程(面向过程编程)都是程序设计的方法,不过稍有区别。

面向过程编程:

1
2
3
4
5
6
7
8
9
1. 导入各种外部库
2. 设计各种全局变量
3. 写一个函数完成某个功能
4. 写一个函数完成某个功能
5. 写一个函数完成某个功能
6. 写一个函数完成某个功能
7. 写一个函数完成某个功能
8. ......
9. 写一个main函数作为程序入口

在多函数程序中,许多重要的数据被放置在全局数据区,这样它们可以被所有的函数访问。每个函数都可以具有它们自己的局部数据,将某些功能代码封装到函数中,日后便无需重复编写,仅调用函数即可。从代码的组织形式来看就是根据业务逻辑从上到下垒代码 。

面向对象编程:

1
2
3
4
5
6
1. 导入各种外部库
2. 设计各种全局变量
3. 决定你要的类
4. 给每个类提供完整的一组操作
5. 明确地使用继承来表现不同类之间的共同点
6. 根据需要,决定是否写一个main函数作为程序入口

面向对象编程中,将函数和变量进一步封装成类,类才是程序的基本元素,它将数据和操作紧密地连结在一起,并保护数据不会被外界的函数意外地改变。类和和类的实例(也称对象)是面向对象的核心概念,是和面向过程编程、函数式编程的根本区别。

并不是非要用面向对象编程,要看你的程序怎么设计方便,但是就目前来说,基本上都是在使用面向对象编程。

0x1类的基本用法

面向对象是通过定义class类来定义,这么说面向对象编程就是只使用class类,在class类中有封装,继承的功能,并且还可以构造要传入的参数,方便控制。
案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
import time
class studetn:
# 定义一个类名为studetn
def __init__(self,idx):
# 定义初始化构造,这里使用init,还有别的属性比如reversed,iter之类的
self.idx=idx
# 初始化变量,方便继承
def runx(self):
# 定义运行函数,从上面继承变量
print(self.idx)
# 打印出idx的值,或者做一些别的处理
time.sleep(1)
a=studetn('a')
a.runx()
# 这是类的调用,一定要记得类的使用方法,首先传入参数,类赋值给一个变量a
# 然后调用这个类下面定义的函数

一些专业术语概念,既然有面向对象编程这个高大上的定义了,自然要搭配一些高大上的概念。

类(Class): 用来描述具有相同属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。其中的对象被称作类的实例。

实例:也称对象。通过类定义的初始化方法,赋予具体的值,成为一个”有血有肉的实体”。

实例化:创建类的实例的过程或操作。

实例变量:定义在实例中的变量,只作用于当前实例。

类变量:类变量是所有实例公有的变量。类变量定义在类中,但在方法体之外。

数据成员:类变量、实例变量、方法、类方法、静态方法和属性等的统称。

方法:类中定义的函数。

静态方法:不需要实例化就可以由类执行的方法

类方法:类方法是将类本身作为对象进行操作的方法。

方法重写:如果从父类继承的方法不能满足子类的需求,可以对父类的方法进行改写,这个过程也称override。

封装:将内部实现包裹起来,对外透明,提供api接口进行调用的机制

继承:即一个派生类(derived class)继承父类(base class)的变量和方法。

多态:根据对象类型的不同以不同的方式进行处理。

0x2调用类的三种方法

0x2.1实例方法

1
2
3
4
5
6
7
8
9
10
import requests

class dd:
def __init__(self,url):
self.url=url
def runx(self):
print(requests.get(self.url).status_code)

a = dd('http://www.baidu.com')
a.runx()

0x2.2静态方法

1
2
3
4
5
6
7
import requests
class ff:
@staticmethod
def runx():
print(requests.get('http://www.baidu.com').status_code)
ff.runx()
#这里就直接调用了类的变量,只在类中运行而不在实例中运行的方法

经常有一些跟类有关系的功能但在运行时又不需要实例和类参与的情况下需要用到静态方法. 比如更改环境变量或者修改其他类的属性等能用到静态方法. 这种情况可以直接用函数解决, 但这样同样会扩散类内部的代码,造成维护困难。
0x2.3类方法
类方法由类调用,采用@classmethod装饰,至少传入一个cls(代指类本身,类似self)参数。执行类方法时,自动将调用该方法的类赋值给cls。建议只使用类名.类方法的调用方式。(虽然也可以使用实例名.类方法的方式调用)

实际案例
如果要构造一个类,接受一个网站和这个网站的状态码,然后打印出来。就像这样:

1
2
3
4
5
6
7
8
9
10
import requests
class gg:
def __init__(self,url,stat):
self.url=url
self.stat=stat
def outer(self):
print(self.url)
print(self.stat)
a = gg('hello',200)
a.outer()

这样就是使用实例方法,虽然可以实现,但是有的时候传入的参数并不是(‘langzi’,200)这样的格式,而是(‘langzi-200’)这样的,那该怎么做?首先要把这个拆分,但是要使用实例方法实现起来很麻烦,这个时候就可以使用类方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import requests
class gg:
url = 0
stat = 0
# 因为使用classmethod后会传入新的变量,所以一开始是需要自己先定义类变量
def __init__(self,url=0,stat=0):
# 这里按照正常的定义构造函数
self.url=url
self.stat=stat
@classmethod
# 装饰器,立马执行下面的函数
def split(cls,info):
# 这个函数接受两个参数,默认的cls就是这个类的init函数,info就是外面传入进来的
url,stat=map(str,info.split('-'))
# 这里转换成了格式化的结构
data = cls(url,stat)
# 然后执行这个类第一个方法,这个类构造函数需要传入两个参数,于是就传入了两个参数
return data
# 这里就直接返回了函数结果
def outer(self):
print(self.url)
print(self.stat)

r = gg.split(('hello-200'))
r.outer()
# 这里是调用类方法,与调用实例方法一样

0x3类的属性

  • Python的类的属性一般分为私有属性和公有属性,而Python没有这类关键字,默认情-况下所有的属性都是“公有的”,对公有属性的访问没有任何限制,且都会被子类继承,也能从子类中进行访问。
  • 若不希望类中的属性在类外被直接访问,就要定义为私有属性。Python使用约定属性名称来划分属性类型。若属性的名字以两个下划线开始,表示私有属性;反之,没有使用双下划线开始的表示公有属性。类的方法也同样使用这样的约定
  • 另外,Python没有保护类型的修饰符。
  • 实例属性是以self为前缀的属性,没有该前缀的属性是普通的局部变量。
  • 在Python中静态变量称为类变量,类变量可以在该类的所有实例中被共享。

类属性仅与其被定义的的类相绑定,主要包括数据属性(静态变量)和方法。方法在类中定义,但却只能被实例调用。如果没有与类的数据属性同名的实例属性,通过实例也可以访问类的数据属性,但是却不能作修改。

类属性和实例属性

私有变量

内置属性

1
2
3
4
5
6
C.__name__  类C的名字(字符串)
C.__doc__ 类C的文档字符串
C.__bases__ 类C的所有父类构成的元组
C.__dict__ 类C的属性
C.__module__ 类C定义所在的模块
C.__class__ 实例C对应的类

范例:

python面向对象编程
实例仅拥有数据属性,即类属性。实例属性可以动态创建,但是如果属性在条件语句中创建,而该条件语句未执行,则该属性实际并不存在,如果在后面代码访问该属性,就会出错。
带默认参数的init(self,…)可以更有效的初始化一个实例,这样可以省去显式传值的麻烦,但是默认参数应当是不变的对象,在使用入列表和字典等可变对象时应时刻保持警惕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class demo_list:
def __init__(self, l=[]):
self.l = l
def add(self, ele):
self.l.append(ele)
def appender(ele):
obj = demo_list()
obj.add(ele)
print(obj.l)
if __name__ == "__main__":
for i in range(5):
appender(i)

output:
[0]
[0, 1]
[0, 1, 2]
[0, 1, 2, 3]
[0, 1, 2, 3, 4]

因为默认参数只会计算一次,不会重复使用,在上面的例子中,虽然使用不同的新建实例,可因为构造器的参数使用的是对列表的引用作为默认参数,所以每次的实例属性都指向该列表所在的空间。

点击查看更多相关资料

使用内建函数dir()可以显示类和实例属性,实例具有dict的特殊属性,该属性由字典组成,包含一个实例的所有属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ReturnInt:
def __init__(self):
pass
ri=ReturnInt()
ri.name='a'
ri.age=10
print(ri.__dict__)
print(dir(ri))



output:
{'name': 'a', 'age': 10}
['__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']

从上面的程序中可以看到除了我们添加的实例属性,实例还有许多其他的属性,这些属性是实例的内建类型属性。

0x4类的方法

  • 类的方法也分为公有方法和私有方法。私有方法不能被模块外的类或方法调用,私有方法也不能被外部的类或函数调用。
  • C++中的静态方法使用关键字static声明,而Python使用函数staticmethod()或@staticmethod修饰器将普通的函数转换为静态方法。Python的静态方法并没有和类的实例进行名称绑定,要调用除了使用通常的方法,使用类名作为其前缀亦可

0x5类的对象信息

type() 可以检查类型。用法超级简单

1
2
3
4
>>> type(123)
<class 'int'>
>>> type('helloworld')
<class 'str'>

对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数。

1
object -> Animal -> Dog -> Husky

那么,isinstance()就可以告诉我们,一个对象是否是某种类型。这玩意儿也是上手熟系列:

1
2
3
4
5
>>> a = Animal()
>>> b = Dog()

>>> isinstance(c, Animal)
True

使用内建函数dir()可以显示类和实例属性,实例具有dict的特殊属性,该属性由字典组成,包含一个实例的所有属性。

0x6类的特性

0x6.1封装
封装是指将数据与具体操作的实现代码放在某个对象内部,外部无法访问。必须要先调用类的方法才能启动。
案例:

1
2
3
4
5
6
7
8
9
10
11
12
class cc:
ccc = 'ccc'
# cc就是类名 如果想要继承别的类 就class cc(threading) 意思就是从threading继承
def __init__(self,a,b,c):
self.a=a
self.b=b
self.c=c
e=cc(1,2,3)
print(e.ccc)
#类变量,在类里面找到定义的变量。
print(ccc)
# 这里会报错,这就是封装。类中的函数同理。

0x6.2继承
当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。
比如,我们已经编写了一个名为Animal的class,有一个run()方法可以直接打印

1
2
3
class Animal(object):
def run(self):
print 'Animal is running...'

当我们需要编写Dog和Cat类时,就可以直接从Animal类继承:

1
2
3
4
class Dog(Animal):
pass
class Cat(Animal):
pass

继承有什么好处?最大的好处是子类获得了父类的全部功能。由于Animial实现了run()方法,因此,Dog和Cat作为它的子类,什么事也没干,就自动拥有了run()方法:

1
2
3
4
dog = Dog()
dog.run()
cat = Cat()
cat.run()

当子类和父类都存在相同的run()方法时,我们说,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()。这样,我们就获得了继承的另一个好处:多态。

多重继承

1
2
3
多重继承的语法格式:class_name(parent_class1, parent_class2…)

其中class_name是类名,parent_class1和parent_class2是父类名。

多重继承关系中的构造函数: 子类从多个父类派生,而子类又没有自己的构造函数时:
(1)按顺序继承,哪个父类在最前面且它又有自己的构造函数,就继承它的构造函数; (2)如果最前面第一个父类没有构造函数,则继承第2个的构造函数,第2个没有的话,再往后找,以此类推。

范例:

0x6.3多态
要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Animal类型的变量:

1
2
3
def run_twice(animal):
animal.run()
animal.run()

当我们传入Animal的实例时,run_twice()就打印出:

1
2
3
4
run_twice(Animal())
运行结果:
Animal is running...
Animal is running...

当我们传入Dog的实例时,run_twice()就打印出:

1
2
3
4
run_twice(Dog())
运行结果:
Dog is running...
Dog is running...

当我们传入Cat的实例时,run_twice()就打印出:

1
2
3
4
run_twice(Cat())
运行结果:
Cat is running...
Cat is running...

看上去没啥意思,但是仔细想想,现在,如果我们再定义一个Tortoise类型,也从Animal派生:

1
2
3
class Tortoise(Animal):
def run(self):
print 'Tortoise is running slowly...'

当我们调用run_twice()时,传入Tortoise的实例:

1
2
3
4
run_twice(Tortoise())
运行结果:
Tortoise is running slowly...
Tortoise is running slowly...

你会发现,新增一个Animal的子类,不必对run_twice()做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。

多态的好处就是,当我们需要传入Dog、Cat、Tortoise……时,我们只需要接收Animal类型就可以了,因为Dog、Cat、Tortoise……都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法,这就是多态的意思:

对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在Animal、Dog、Cat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:

对扩展开放:允许新增Animal子类;
对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。

继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写;
有了继承,才能有多态。在调用类实例方法的时候,尽量把变量视作父类类型,这样,所有子类类型都可以正常被接收;
旧的方式定义Python类允许不从object类继承,但这种编程方式已经严重不推荐使用。任何时候,如果没有合适的类可以继承,就继承自object类。

0x7成员保护与访问机制

有些对象你不想外部访问,即使是通过调用类对象也无法访问
0x7.1私有成员

1
2
3
4
5
6
7
8
9
10
11
12
class obj:
def __init__(self,name):
self.name=name
def pri(self):
print self.name
__age = 18
# 加上双下划线的就是私有变量,只能在类的内部访问,外部无法访问
a = obj('zhao')
a.pri()
运行结果:

zhao

如果要在类中调用这个私有成员,可以这么用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class obj:
def __init__(self,name):
self.name=name
def prin(self):
print self.name
__age = 18
# 加上双下划线的就是私有变量,只能在类的内部访问,外部无法访问
@classmethod
# 如果要在类中调用,首先调用类方法
def pri(cls):
print cls.__age
# 然后在使用
a = obj('zhao')
a.prin()
obj.pri()
# 通过这样直接调用类中的私有变量
运行结果:

zhao
18
```
不过实际上还是可以直接访问的。

**0x7.2使用get-set-del方法操作私有成员**
```bash
class obj:
def __init__(self,name):
self.name=name
def prin(self):
print self.name
__age = 18
# 加上双下划线的就是私有变量,只能在类的内部访问,外部无法访问
@classmethod
# 如果要在类中调用,首先调用类方法
def pri(cls):
print cls.__age
# 然后在使用
@classmethod
def set_age(cls,value):
cls.__age = value
return cls.__age
# 这个用法就是改变__age的值
@classmethod
def get_age(cls):
return cls.__age
# 这个用法就是直接返回__age的值
@classmethod
def del_age(cls):
del cls.__age
# 这个用法就是直接删除__age的值

print obj.get_age()
# 这里是直接调用出__age的值 返回值18
print obj.set_age(20)
# 这里是直接改变__age的值 返回值20
obj.del_age()
# 这里是直接删除__age的值

思考: 既然是私有变量,不让外部访问,为何有要在后面调用又改变呢?因为可以对私有变量进行额外的检测,处理,加工等等。比如判断value的值,使用isinstance然后做if-else判断。

使用私有变量可以对内部变量进行保护,外部无法改变,但是可以对它进行检测处理。

这里引申一下私有成员的保护机制,使用age对私有变量其实就是—>obj._objage的样子进行保护,说白了你直接使用obj._obj__age就可以直接调用内部私有变量age了。

0x8Propety装饰器

把类的方法伪装成属性调用的方式,就是把类里面的一个函数,变成一个属性一样的东西~
一开始调用类的方法要使用圆括号,现在变成了属性进行读取设置存储。
举个例子来说明:

常用调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class obj:
def __init__(self,name,age):
self.__name=name
self.__age=age
# 讲这些设置成私有变量
def get_age(self):
return self.__age
def set_age(self,value):
if isinstance(value,int):
self.__age=value
else:
raise ValueError('非整数类型')
def del_age(self):
print('delete over')
a = obj('langzi',18)
print(a.get_age())
a.set_age(20)
print(a.get_age())

使用装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class obj:
def __init__(self,name,age):
self.__name=name
self.__age=age
# 把这些设置成私有变量
@property
def age(self):
return self.__age
@age.setter
def age(self,value):
if isinstance(value,int):
self.__age=value
else:
raise ValueError('非整数类型')
@age.deleter
def age(self):
print('delete over')
a = obj('langzi',18)
# 使用这些装饰器,可以使用类与对象的方法直接调用
print(a.age)
# 这里就是直接调用返回age的值
a.age=20
# 这里就是直接使用setter把值转换
print(a.age)
del a.age
# 删除age

当然这种调用方法有些麻烦,每次都是一个一个去实例类与对象,有个更加简单直观的方法。

更加减半的使用property()函数
除了使用装饰器的方式将一个方法伪装成属性外,Python内置的builtins模块中的property()函数,为我们提供了第二种设置类属性的手段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class People:

def __init__(self, name, age):
self.__name = name
self.__age = age

def get_age(self):
return self.__age

def set_age(self, age):
if isinstance(age, int):
self.__age = age
else:
raise ValueError

def del_age(self):
print("删除年龄数据!")

# 核心在这句
age = property(get_age, set_age, del_age, "年龄")


obj = People("jack", 18)
print(obj.age)
obj.age = 19
print("obj.age: ", obj.age)
del obj.age

0x9 __init__(self,…)‘构造器’方法

init(self,…)方法实际上并不是一个构造器,其并没有创造一个新的对象,Python通过函数操作符()创建对象。在解释器创建一个实例后,最先调用init(self,…)方法,定义额外的行为,如果没有定义或者覆盖init(self,…)方法,对实例不会施加任何特别的操作,直接返回它的对象,实例化过程完毕。函数的返回值应当为None,如果返回来任意对象,会导致TypeError异常。

1
2
3
4
5
6
7
8
9
10
11
12
class ReturnInt:
def __init__(self):
return 1

ri=ReturnInt()


output:
Traceback (most recent call last):
File "E:\code\python27\test12121.py", line 6, in <module>
ri=ReturnInt()
TypeError: __init__() should return None, not 'int'

重写子类的init(self,…)方法不会自动调用基类的init(self,…)。所以如果需要在子类中调用基类的init(self,…),需要明确指出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class P():
def __init__(self):
self.age=20
self.name='test'

class C(P):
def __init__(self):
P.__init__(self)
c=C()
print(c.age)
print(c.name)


output:
20
test

我们可以使用super()函数更方便高效的重写上面的代码:

1
2
3
class C(P):
def __init__(self):
super(C,self).__init__()

不需要提供明确的父类,super()函数会帮助我们找到相应的父类,然后方便调用相关的属性。

0x10使用枚举类

当我们需要定义常量时,一个办法是用大写变量通过整数来定义,例如月份:

1
2
3
4
5
6
JAN = 1
FEB = 2
MAR = 3
...
NOV = 11
DEC = 12

好处是简单,缺点是类型是int,并且仍然是变量。

更好的方法是为这样的枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。Python提供了Enum类来实现这个功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from enum import Enum

Month = Enum('Month', (
'Jan', 'Feb', 'Mar', 'Apr',
'May', 'Jun', 'Jul', 'Aug',
'Sep', 'Oct', 'Nov', 'Dec'
))
for name, member in Month.__members__.items():
print(name, '=>', member, ',', member.value)


output:
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

这样我们就获得了Month类型的枚举类,可以直接使用Month.Jan来引用一个常量,或者枚举它的所有成员
value属性则是自动赋给成员的int常量,默认从1开始计数。

如果需要更精确地控制枚举类型,可以从Enum派生出自定义类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from enum import Enum, unique

# @unique装饰器可以帮助我们检查保证没有重复值。
@unique
class Weekday(Enum):
Sun = 0 # Sun的value被设定为0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6

for name, member in Weekday.__members__.items():
print(name, '=>', member,member.value)

0x11使用元类

动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。

比方说我们要定义一个Hello的class,就写一个hello.py模块:

1
2
3
class Hello(object):
def hello(self, name='world'):
print('Hello, %s.' % name)

当Python解释器载入hello模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Hello的class对象,测试如下:

1
2
3
4
5
6
7
8
>>> from hello import Hello
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class 'hello.Hello'>

type()函数可以查看一个类型或变量的类型,Hello是一个class,它的类型就是type,而h是一个实例,它的类型就是class Hello。

class的定义是运行时动态创建的,而创建class的方法就是使用type()函数。

type()函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过type()函数创建出Hello类,而无需通过class Hello(object)…的定义:

1
2
3
4
5
6
7
8
9
10
11
>>> def fn(self, name='world'): # 先定义函数
... print('Hello, %s.' % name)
...
>>> Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class '__main__.Hello'>

要创建一个class对象,type()函数依次传入3个参数:

1
type(‘Hello’, (object,), dict(hello=fn))
  • class名称;
  • 继承父类的集合,注意Python支持多重继承,别忘了tuple的单元素写法;
  • class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上。

通过type()函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()函数创建出class。

正常情况下,我们都用class Xxx…来定义类,但是,type()函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。

除了使用type()动态创建类以外,要控制类的创建行为,还可以使用metaclass。

metaclass,直译为 元类 ,简单的解释就是:

当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。

但是如果我们想创建出类呢?那就必须根据metaclass创建出类,所以:先定义metaclass,然后创建类。

连接起来就是:先定义metaclass,就可以创建类,最后创建实例。

所以,metaclass允许你创建类或者修改类。换句话说,你可以把类看成是metaclass创建出来的“实例”。

来个例子感受一下,按照默认习惯,metaclass的类名总是以Metaclass结尾,以便清楚地表示这是一个metaclass:

1
2
3
4
5
# metaclass是类的模板,所以必须从`type`类型派生:
class ListMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(cls, name, bases, attrs)

有了ListMetaclass,我们在定义类的时候还要指示使用ListMetaclass来定制类,传入关键字参数metaclass:

1
2
class MyList(list, metaclass=ListMetaclass):
pass

当我们传入关键字参数metaclass时,魔术就生效了,它指示Python解释器在创建MyList时,要通过ListMetaclass.new()来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后,返回修改后的定义。

new()方法接收到的参数依次是:

当前准备创建的类的对象;

  • 类的名字;
  • 类继承的父类集合;
  • 类的方法集合。

0x13抽象类

抽象类中只能有抽象方法,子类继承抽象类时,不能通过实例化使用其抽象方法,必须实现该方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from abc import ABCMeta,abstractmethod #
class Fruit(metaclass=ABCMeta):
@abstractmethod
def grow(self):
pass
class Apple(Fruit):
def grow(self):
print('Apple growing')
if __name__=="__main__":
apple=Apple()
apple.grow()

output:
Apple growing

0x14类的内置方法

在上面有提到除了init之外还有iter,reverse的方法,这里就详细说下除了init初始化还有哪些别的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__init__ :      构造函数,在生成对象时调用
__del__ : 析构函数,释放对象时使用
__repr__ : 打印,转换
__setitem__ : 按照索引赋值
__getitem__: 按照索引获取值
__len__: 获得长度
__cmp__: 比较运算
__call__: 调用
__add__: 加运算
__sub__: 减运算
__mul__: 乘运算
__div__: 除运算
__mod__: 求余运算
__pow__: 幂

具体使用:

doc

说明性文档和信息。Python自建,无需自定义。

1
2
3
4
5
6
7
8
9
10
class Foo:
""" 描述类信息,可被自动收集 """
def func(self):
pass
# 打印类的说明文档
print(Foo.__doc__)


output:
描述类信息,可被自动收集

init()

实例化方法,通过类创建实例时,自动触发执行。

1
2
3
4
5
class Foo:
def __init__(self, name):
self.name = name
self.age = 18
obj = Foo(jack') # 自动执行类中的 __init__ 方法

module__ 和 __class

module 表示当前操作的对象在属于哪个模块。
class 表示当前操作的对象属于哪个类。
这两者也是Python内建,无需自定义。

1
2
3
4
5
6
7
8
9
class Foo:
pass
obj = Foo()
print(obj.__module__)
print(obj.__class__)

output:
__main__
<class '__main__.Foo'>

del()

析构方法,当对象在内存中被释放时,自动触发此方法。

注:此方法一般无须自定义,因为Python自带内存分配和释放机制,除非你需要在释放的时候指定做一些动作。析构函数的调用是由解释器在进行垃圾回收时自动触发执行的。

1
2
3
4
5
6
7
class Foo:
def __del__(self):
print("我被回收了!")

obj = Foo()
del obj
# call()

call()

如果为一个类编写了该方法,那么在该类的实例后面加括号,可会调用这个方法。

注:构造方法的执行是由类加括号执行的,即:对象 = 类名(),而对于call() 方法,是由对象后加括号触发的,即:对象() 或者 类()()

1
2
3
4
5
6
7
class Foo:
def __init__(self):
pass
def __call__(self, *args, **kwargs):
print('__call__')
obj = Foo() # 执行 __init__
obj() # 执行 __call__

可以用Python内建的callable()函数进行测试,判断一个对象是否可以被执行。

1
callable(Student())

运行结果:

1
True

dict

列出类或对象中的所有成员!非常重要和有用的一个属性,Python自建,无需用户自己定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Province:
country = 'China'
def __init__(self, name, count):
self.name = name
self.count = count
def func(self, *args, **kwargs):
print('func')
# 获取类的成员
print(Province.__dict__)
# 获取 对象obj1 的成员
obj1 = Province('HeBei',10000)
print(obj1.__dict__)
# 获取 对象obj2 的成员
obj2 = Province('HeNan', 3888)
print(obj2.__dict__)


output:
{'__module__': '__main__', 'country': 'China', '__init__': <function Province.__init__ at 0x000001B139C6A048>, 'func': <function Province.func at 0x000001B139C6A1E0>, '__dict__': <attribute '__dict__' of 'Province' objects>, '__weakref__': <attribute '__weakref__' of 'Province' objects>, '__doc__': None}
{'name': 'HeBei', 'count': 10000}

str()

如果一个类中定义了str()方法,那么在打印对象时,默认输出该方法的返回值。这也是一个非常重要的方法,需要用户自己定义。 

下面的类,没有定义str()方法,打印结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo:
pass
obj = Foo()
print(obj)
#定义了__str__()方法后,打印结果是:'jack'。
class Foo:
def __str__(self):
return 'jack'
obj = Foo()
print(obj)

output:
<__main__.Foo object at 0x00000267B25CC780>
jack

getitem__()、setitem()、__delitem()

取值、赋值、删除这“三剑客”的套路,在Python中,我们已经见过很多次了,比如前面的@property装饰器。

Python中,标识符后面加圆括号,通常代表执行或调用方法的意思。而在标识符后面加中括号[],通常代表取值的意思。Python设计了getitem()、setitem()、delitem()这三个特殊成员,用于执行与中括号有关的动作。它们分别表示取值、赋值、删除数据。

也就是如下的操作:

a = 标识符[] :   执行getitem方法
标识符[] = a :   执行setitem方法
del 标识符[] :   执行delitem方法
如果有一个类同时定义了这三个魔法方法,那么这个类的实例的行为看起来就像一个字典一样,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
class Foo:
def __getitem__(self, key):
print('__getitem__',key)
def __setitem__(self, key, value):
print('__setitem__',key,value)
def __delitem__(self, key):
print('__delitem__',key)
obj = Foo()
result = obj['k1'] # 自动触发执行 __getitem__
obj['k2'] = 'jack' # 自动触发执行 __setitem__
del obj['k1'] # 自动触发执行 __delitem__

iter()

这是迭代器方法!列表、字典、元组之所以可以进行for循环,是因为其内部定义了 iter()这个方法。如果用户想让自定义的类的对象可以被迭代,那么就需要在类中定义这个方法,并且让该方法的返回值是一个可迭代的对象。当在代码中利用for循环遍历对象时,就会调用类的这个iter()方法。

普通的类:

1
2
3
4
5
6
7
class Foo:
pass
obj = Foo()
for i in obj:
print(i)
# 报错:TypeError: 'Foo' object is not iterable
# 原因是Foo对象不可迭代

添加一个iter(),但什么都不返回:

1
2
3
4
5
6
7
8
class Foo:
def __iter__(self):
pass
obj = Foo()
for i in obj:
print(i)
# 报错:TypeError: iter() returned non-iterator of type 'NoneType'
#原因是 __iter__方法没有返回一个可迭代的对象

返回一个个迭代对象:

1
2
3
4
5
6
7
8
class Foo:
def __init__(self, sq):
self.sq = sq
def __iter__(self):
return iter(self.sq)
obj = Foo([11,22,33,44])
for i in obj:
print(i)

最好的方法是使用生成器:

1
2
3
4
5
6
7
8
9
10
class Foo:
def __init__(self):
pass
def __iter__(self):
yield 1
yield 2
yield 3
obj = Foo()
for i in obj:
print(i)

len()

在Python中,如果你调用内置的len()函数试图获取一个对象的长度,在后台,其实是去调用该对象的len()方法,所以,下面的代码是等价的:

1
2
3
4
len('ABC')
3
'ABC'.__len__()
3

Python的list、dict、str等内置数据类型都实现了该方法,但是你自定义的类要实现len方法需要好好设计。

repr()

这个方法的作用和str()很像,两者的区别是str()返回用户看到的字符串,而repr()返回程序开发者看到的字符串,也就是说,repr()是为调试服务的。通常两者代码一样。

1
2
3
4
5
6
class Foo:
def __init__(self, name):
self.name = name
def __str__(self):
return "this is %s" % self.name
__repr__ = __str__

add__: 加运算 sub: 减运算 mul: 乘运算 div: 除运算 mod: 求余运算 __pow: 幂运算

这些都是算术运算方法,需要你自己为类设计具体运算代码。有些Python内置数据类型,比如int就带有这些方法。Python支持运算符的重载,也就是重写。

1
2
3
4
5
6
7
8
9
10
11
class Vector:
def __init__(self, a, b):
self.a = a
self.b = b
def __str__(self):
return 'Vector (%d, %d)' % (self.a, self.b)
def __add__(self,other):
return Vector(self.a + other.a, self.b + other.b)
v1 = Vector(2,10)
v2 = Vector(5,-2)
print (v1 + v2)

author作者信息

1
2
3
4
__author__ = "Jack"
def show():
print(__author__)
show()

slots

Python作为一种动态语言,可以在类定义完成和实例化后,给类或者对象继续添加随意个数或者任意类型的变量或方法,这是动态语言的特性。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def print_doc(self):
print("haha")

class Foo:
pass

obj1 = Foo()
obj2 = Foo()
# 动态添加实例变量
obj1.name = "jack"
obj2.age = 18
# 动态的给类添加实例方法
Foo.show = print_doc
obj1.show()
obj2.show()

但是!如果我想限制实例可以添加的变量怎么办?可以使slots限制实例的变量,比如,只允许Foo的实例添加name和age属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def print_doc(self):
print("haha")
class Foo:
__slots__ = ("name", "age")
pass
obj1 = Foo()
obj2 = Foo()
# 动态添加实例变量
obj1.name = "jack"
obj2.age = 18
obj1.sex = "male" # 这一句会弹出错误
# 但是无法限制给类添加方法
Foo.show = print_doc
obj1.show()
obj2.show()
由于'sex'不在__slots__的列表中,所以不能绑定sex属性,试图绑定sex将得到AttributeError的错误。
Traceback (most recent call last):
File "F:/Python/pycharm/201705/1.py", line 14, in <module>
obj1.sex = "male"
AttributeError: 'Foo' object has no attribute 'sex'

需要提醒的是,slots定义的属性仅对当前类的实例起作用,对继承了它的子类是不起作用的。想想也是这个道理,如果你继承一个父类,却莫名其妙发现有些变量无法定义,那不是大问题么?如果非要子类也被限制,除非在子类中也定义slots,这样,子类实例允许定义的属性就是自身的slots加上父类的slots。
参考文章:https://mp.weixin.qq.com/s/jEKvWwu5MwCqLbIxmAP4iQ
Python 面向对象编程容易忽视的知识点:https://www.jianshu.com/p/505117d9a693
陷阱!python参数默认值https://segmentfault.com/a/1190000000743526
一篇文章搞懂python面向对象编程:http://yangcongchufang.com/%E9%AB%98%E7%BA%A7python%E7%BC%96%E7%A8%8B%E5%9F%BA%E7%A1%80/python-object-class.html
Python面向对象编程https://zhuanlan.zhihu.com/p/34305989

FROM :blog.cfyqy.com | Author:cfyqy

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月6日01:34:07
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   python面向对象编程http://cn-sec.com/archives/721868.html

发表评论

匿名网友 填写信息