类(Class): 用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例。
对象:通过类定义的数据结构实例。对象包括两个数据成员(类变量和实例变量)和方法。
类变量:类变量在整个实例化的对象中是公用的。类变量定义在类中且在函数体之外。类变量通常不作为实例变量使用。
实例变量:定义在方法中的变量,只作用于当前实例的类。
对“类”和“对象”的使用:
- 类就是一个模板,模板里可以包含多个函数,函数里实现一些功能。
- 对象则是根据模板创建的实例,通过实例对象可以执行类中的函数。
#创建类
class Foo:
# 类中的函数
def bar(self):
#功能阐述
pass
# =====完毕========
#根据Foo创建对象obj
obj = Foo()
#创建对象的时候 记得后面加个括号
注意,按照Python通用规则,Class用驼峰式表示(HelloWorld)
而其他的obj等等,都用‘_’隔开(this_is_object)
类中的函数第一个参数必须是self,类中定义的函数叫做“方法”。
创建类
class Foo:
def bar(self):
print('Bar')
def hello(self, name):
print('i am %s' %name)
# 根据Foo创建的对象
obj = Foo()
obj.Bar() #执行Bar方法
obj.Hello('july') #执行Hello方法
Bar
i am july
self 是个什么鬼呢?它是为了指代它所存在的类Class之中。
比如我们如果有好几个不同的obj被创建成同一个类,
那么有了self,我们的class Foo就能很好的知道哪个指的是自己,不会乱
# 创建类
class Foo:
# 这里我们可以创建一个类级别的变量
# 它不会随着由此类创建的变量而变化
name = 'Jan'
def bar(self):
print('Bar')
def hello(self, name):
print('you are %s' %self.name)
print('I am %s' %name)
print('\n')
# 根据Foo创建的对象
obj1 = Foo()
obj2 = Foo()
obj1.hello('August')
obj2.hello('July')
you are Jan
I am August
you are Jan
I am July
所以说,这个 self 就是个代指。代指了自己所在的class。你可以由 self 点进所指class本身的函数。由此可见,self 本身作为一个代词,并不一定要叫self。你也可以用个其他什么来代替。只不过,必须得是这个类的所有子方法的第一个参数:
# 创建类
class Foo:
# 这里我们可以创建一个类级别的变量
# 它不会随着由此类创建的变量而变化
name = 'Jan'
def bar(july):
print('Bar')
def hello(july, name): # 我这里把self改成了july,
# 但是只要它作为第一参数的位置没变,它依旧是Foo Class的自我指代
print('you are %s' %july.name)
print('I am %s' %name)
print('\n')
# 根据Foo创建的对象
obj1 = Foo()
obj2 = Foo()
obj1.hello('August')
obj2.hello('July')
you are Jan
I am August
you are Jan
I am July
构造函数:构造函数,是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值。
跟所有OOP语言一样,python也是有构造函数的,默认为:
# 创建类
class Foo:
def __init__(self):#这就是构造函数,它的职责是在模型创建的初期,就完成一些动作
#简单的说就是,自定义的初始化步骤:
#同样,它需要self来指代本身这个class
self.name='Jan'
def hello(self, name):
print('you are %s' %self.name)
print('I am %s' %name)
print('\n')
# ==== 完毕 =====
#当你创建一个Foo类的时候,init会被自动跑一遍:
obj = Foo()
# 在我们的例子中,我们默认给self自己的name变量,赋值为’JAN‘
# 此刻,当我们调用Foo的hello()方法时,hello自己的name变量,被赋值为'July'
obj.hello('July')
you are Jan
I am July
init是可以带更多的参数的,用以初始化我们的class本身。
比如说,你要初始化一个类的时候要用到一些外部参数:
# 创建类
class Foo:
def __init__(self, name2):# 你可以在这里附加上一些参数
# 这些参数将是你创建一个Foo类时的必要条件
self.name=name2
def hello(self, name):
print('you are %s' %self.name)
print('I am %s' %name)
print('\n')
# ==== 完毕 =====
#当你创建一个Foo类的时候,init会被自动跑一遍:
# 此刻,你不可以直接跑Foo(),你需要填入一个参数:name2
obj = Foo('Feb')
# 然后再调用hello, 并赋值July
obj.hello('July')
you are Feb
I am July
由楼上这些例子,我们大概可以知道整个Python的OOP概念了:
Class(类)就是一个把一堆Object(对象?)集合起来的地方。
在这里,无论是变量还是方法,他们享有基本一样的层级概念。只不过,方法要做一点事儿,而变量直接就是一个值。
访问限制
我们刚刚看到,在调用obj的时候,可以直接调出name或者使用hello()。那么我们怎么知道什么时候可以调用他们,什么时候不可以呢?
在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问
举个学生的例子,我们可以用一个学生类来存储学生的信息,但是我们在外部可以接触到name,那么其实我们就是可以直接修改name的,这是不安全的
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def detail(self):
print(self.name)
print(self.age)
LiLei = Student('LiLei', 12)
LiLei.age = 20
LiLei.detail()
LiLei
20
为了防止这种篡改年龄的事情发生,为了维护世界的和平,我们需要把关键的信息给做好隐藏:
class Student:
def __init__(self, name, age):
self.__name = name
self.__age = age
def detail(self):
print(self.__name)
print(self.__age)
LiLei = Student('LiLei', 12)
LiLei.__age = 20
LiLei.detail()
LiLei
12
看,如此一来,年龄就不会被更改了。
那么如何既保证安全,又能被外部修改呢?
我们使用OOP家族传统理念:Getter+Setter
class Student(object):
...
def get_name(self):
return self.__name
def get_age(self):
return self.__age
class Student(object):
...
def set_age(self, score):
self.__age = age
至此,我们应该学会使用Class来定义我们自己的类了
接下来,我们来看看:
在Python中展现面向对象三大特性:
面向对象的三大特性是指:封装、继承和多态。
封装
指的就是把内容封装到某个地方,用于日后调用
它需要:
-
把内容封装在某处
-
从另一处调用被封装的内容
通过对象直接调用
我们可以在存完一个内容以后,在类以外的地方,通过这个类的对象,来直接”点“调用
class Student:
# 假定我们初始化一个Student类的时候要做的就是,记录下每个学生的名字和年龄
def __init__(self, name, age):
self.name = name
self.age = age
# 至此,我们用self指代student本身,并用name和age存下了他们的年龄和名字
# === 完毕 ===
#此时,我们新建一个学生
obj1 = Student('July', 18)
print(obj1.name) # 直接调用obj1对象的name属性
print(obj1.age) # 直接调用obj1对象的age属性
obj2 = Student('Aug', 73)
print(obj2.name) # 直接调用obj2对象的name属性
print(obj2.age) # 直接调用obj2对象的age属性
July
18
Aug
73
通过self间接调用
执行类中某一个方法时,通过self来调用了类自己的变量
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def detail(self):
print(self.name)
print(self.age)
# === 完毕 ===
#此时,我们新建一个学生
obj1 = Student('July', 18)
obj1.detail() #Python默认将obj1传给self,所以其实我们做的是obj1.detail(obj1)
# 那么,detail()内部的样貌其实就是:
# print(obj1.name)
# print(obj1.age)
obj2 = Student('Aug', 73)
obj2.detail()
July
18
Aug
73
综上所述,对于面向对象的封装来说,其实就是使用构造方法将内容封装到 对象 中,然后通过对象直接或者self间接获取被封装的内容。
继承
继承,面向对象中的继承和现实生活中的继承相同,即:子可以继承父的内容(爸爸有的儿子都有)。
例如,每个学生都有名字和年龄,木有问题。我们可以把这个作为我们的父亲类。
但是,每个学生自己,可能有自己不同的”方法“,比如,每个人有每个人不同的外号,不同的口号,不同的饮食习惯,不同的。
# 我们首先创建一个学生类,这个类是所有学生的爸爸
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def detail(self):
print(self.name)
print(self.age)
# 然后,我们创建一个小学生类,小学生特点是,LOL sala无敌
class PrimaryStudent(Student):#因为是继承于学生类,所以我们写在括号内
# 这里我们可以不写构造函数,于是我们就是直接沿用Student类的构造函数
def lol(self): # 我们有一些新的独有的方法,会被叠加起来
print('不服sala!')
# 接下来,我们创建一个大学生类,大学生特点是,额,每个人都有个妹子。。
class CollegeStudent(Student):
def __init__(self, name, age, gf): #这里,我们改写一下构造函数
# 于是爸爸的init会被直接overwrite
self.name = name
self.age = age
self.gf = gf
def gf_detail(self):
print(self.gf)
# 来,我们来创建一下
obj1 = PrimaryStudent('小王', 7)
obj1.lol() # 独有的方法
obj1.detail()#继承与爸爸的方法
obj2 = CollegeStudent('王王', 29, '张张')
obj2.detail()
obj2.gf_detail()
不服sala!
小王
7
王王
29
张张
所以,对于面向对象的继承来说,其实就是将多个类共有的方法提取到父类中,子类仅需继承父类而不必一一实现每个方法。
这样可以极大的提高效率,减少代码的重复。
问题来了,如果我想多认个干爹呢?
Python和Java/C#的不同就是,Python可以多类继承,也就是,可以认很多干爹
但是干爹多了,就出了问题了。继承的时候,从谁先开始?
有两种方式,分别是深度优先和广度优先
-
当本身的类是经典类的时候,就按照深度优先方式查找继承的方法 (即,找到一个爸爸,继续找这个爸爸的爸爸,爸爸的爸爸的爸爸。。。)
-
当本身的类是新式类的时候,就按照广度优先的方式查找 (即,找到一个爸爸,再找下一个爸爸,再找下一个爸爸,平辈之间查找)
那么为什么有经典类和新类之分呢?
这是个历史遗留问题,新类 统一了类(class)和类型(type),所以其实也是社区推荐的写法,只不过。。很多程序员都很懒。。
在2.2之前,比如2.1版本中,类和类型是不同的,如a是ClassA的一个实例,那么a.__class__返回 ‘ class __main__.ClassA‘ ,type(a)返回总是<type 'instance'>。而引入新类后,比如ClassB是个新类,b是ClassB的实例,b.__class__和type(b)都是返回‘class '__main__.ClassB' ,这样就统一了。
于是乎,在新版的Python中,这个经典类和新类的区别已经不存在,都统一使用广度优先。
我们先假设我们还活在python2.2的时代:
#经典类的写法
class c1:
pass
class c2(c1):
pass
#新类的写法
class N1(object):
pass
class N2(N1):
pass
可见,新类的标志就是,大家的老祖宗继承于一个系统级的类,叫Object
具体的,我们来看看:
- 经典类
class D:
def bar(self):
print('D.bar')
class C(D):
def bar(self):
print('C.bar')
class B(D):
pass
class A(B, C):
pass
a = A()
# 执行bar方法时
# 首先去A类中查找,如果A类中没有,则继续去B类中找,如果B类中么有,则继续去D类中找,如果D类中么有,则继续去C类中找,如果还是未找到,则报错
# 所以,查找顺序:A --> B --> D --> C
# 在上述查找bar方法的过程中,一旦找到,则寻找过程立即中断,便不会再继续找了
a.bar()
C.bar
- 新类
class D(object):
def bar(self):
print('D.bar')
class C(D):
def bar(self):
print('C.bar')
class B(D):
pass
class A(B, C):
pass
a = A()
# 执行bar方法时
# 首先去A类中查找,如果A类中没有,则继续去B类中找,如果B类中么有,则继续去C类中找,如果C类中么有,则继续去D类中找,如果还是未找到,则报错
# 所以,查找顺序:A --> B --> C --> D
# 在上述查找bar方法的过程中,一旦找到,则寻找过程立即中断,便不会再继续找了
a.bar()
C.bar
当然,对我们先现在而言,两种写法都得出C.bar ;这说明,已经木有区别了。
多态
Pyhon不支持多态并且也用不到多态,多态的概念是应用于Java和C#这一类强类型语言中,而Python崇尚“鸭子类型(Duck Typing)”。
什么是鸭子类型?其实翻译成中文最好是叫:好猫类型。
也就是引用了小平同志的一句话,不管黑猫白猫抓到老鼠的就是好猫。
不同于强类型的语言,一个类型的obj只能一种事儿,
在Python中,只要是能“不报错运行”的类型,都可以塞进参数中去:
class F1:
pass
# 假设,S1是我们的正统类,它继承于根正苗红的F1,是我们的正统类
class S1(F1):
def show(self):
print('S1.show')
# S2是路人甲,是个歪瓜裂枣,但是他自己也有一个叫show的方法。
class S2:
def show(self):
print('S2.show')
# 在Java或C#中定义函数参数时,必须指定参数的类型,也即是说,我们如果用
# Java写下面的Func,需要告知,obj是F1类还是其他什么东西。
# 如果限定了F1,那么S2是不可以被采纳的。
# 然而,在Python中,一切都是Obj,它不care你到底是什么类,直接塞进去就可以
def Func(obj):
"""Func函数需要接收一个F1类型或者F1子类的类型"""
obj.show()
s1_obj = S1()
Func(s1_obj) # 在Func函数中传入S1类的对象 s1_obj,执行 S1 的show方法,结果:S1.show
s2_obj = S2()
Func(s2_obj) # 在Func函数中传入Ss类的对象 ss_obj,执行 Ss 的show方法,结果:S2.show
S1.show
S2.show
实战
我们现在已经完全掌握了使用包,自己定义类,组件一个可运行的程序的方法
现在我们可以专注于Machine Learning方面,来看看实战是怎么运用这些知识的。
from sklearn import svm, datasets
class Dataset:
# 我们创造一个dataset的类,这个类会帮我们下载相关的数据集,
# 并给我们分类好x,y
def __init__(self, name):
# 告诉类,我们需要哪一个数据集
# 我们有两个选择,一个是'iris'一个是'digits'
self.name = name
def download_data(self):
# 从sklearn的自带集中下载我们指定的数据集
if self.name == 'iris':
# 这里是sklearn自带的数据集下载方法,更多信息可以参照官网
self.downloaded_data = datasets.load_iris()
elif self.name == 'digits':
self.downloaded_data = datasets.load_digits()
else:
# 如果不是我们预想的两种数据集,则报错
print('Dataset Error: No named datasets')
def generate_xy(self):
# 通过这个过程来把我们的数据集分为原始数据以及他们的label
# 我们先把数据下载下来
self.download_data()
x = self.downloaded_data.data
y = self.downloaded_data.target
print('\nOriginal data looks like this: \n', x)
print('\nLabels looks like this: \n', y)
return x,y
def get_train_test_set(self, ratio):
# 这里,我们把所有的数据分成训练集和测试集
# 一个参数要求我们告知,我们以多少的比例来分割训练和测试集
# 首先,我们把XY给generate出来:
x, y = self.generate_xy()
# 有个比例,我们首先得知道 一共有多少的数据
n_samples = len(x)
# 于是我们知道,有多少应该是训练集,多少应该是测试集
n_train = n_samples * ratio
# 好了,接下来我们分割数据
X_train = x[:n_train]
y_train = y[:n_train]
X_test = x[n_train:]
y_test = y[n_train:]
# 好,我们得到了所有想要的玩意儿
return X_train, y_train, X_test, y_test
# ====== 我们的dataset类创造完毕=======
接下来,我们在main中code以下来调用我们自己写的类:
# 比如,我们使用digits数据集
data = Dataset('digits')
# 接着,我们可以用0.7的分割率把xy给分割出来
X_train, y_train, X_test, y_test = data.get_train_test_set(0.7)
Original data looks like this:
[[ 0. 0. 5. ..., 0. 0. 0.]
[ 0. 0. 0. ..., 10. 0. 0.]
[ 0. 0. 0. ..., 16. 9. 0.]
...,
[ 0. 0. 1. ..., 6. 0. 0.]
[ 0. 0. 2. ..., 12. 0. 0.]
[ 0. 0. 10. ..., 12. 1. 0.]]
Labels looks like this:
[0 1 2 ..., 8 9 8]
同样,我们也不一定需要自己创造类,我们可以引用第三方库里的类, 比如这里,我们用SVM作为我们的分类器,去训练我们的算法 我们就直接建造一个object,使他成为SVM类
clf = svm.SVC()
这里 clf 是 classifier的简称,SVC指的是SVM的classification版本。
因为我们的数据集都是分类问题,所以我们使用SVC()
接下来,我们fit我们的数据(也就是训练我们的数据)
显然,做fit的时候,我们只可以使用训练集
clf.fit(X_train, y_train)
SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
decision_function_shape=None, degree=3, gamma='auto', kernel='rbf',
max_iter=-1, probability=False, random_state=None, shrinking=True,
tol=0.001, verbose=False)
好,现在你的clf已经训练好了,我们来看看它的表现:
我们随便取test集中的一个数据点,并对应它的真实label
test_point = X_test[12]
y_true = y_test[12]
我们来看看,我们的clf给出的预测是什么:
clf.predict(test_point)
array([7])
再看看真正的label应该是什么:
y_true
7
正确!
那么这样,你们已经学会如何训练数据集并作出新的预测了。
把所有的测试集都导入clf,让他pridict,并看看跟真实的label相差多少。