USF MSDS501 计算数据科学中文讲义 2.8 面向对象编程

来源:ApacheCN『USF MSDS501 计算数据科学中文讲义』翻译项目

原文:Object-oriented programming

译者:飞龙

协议:CC BY-NC-SA 4.0

大揭秘

到目前为止,我们一直在使用函数和函数包,以及定义我们自己的函数。 但事实证明,我们一直在使用对象,我们只是没有认识到它们。 例如,

x = 'Hi'
x.lower()

# 'hi'

字符串x是我们可以发送消息的对象。

print( type(x) )

# <class 'str'>

甚至整数都是对象:

print(dir(99))

# ['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']

是对象的蓝图,基本上是类型的名称,在这种情况下是str。 对象称为类的实例

x.lower()中,我们将lower消息发送到x字符串对象。 消息实际上只是与类/对象相关的函数。

x.lower

# <function str.lower>

在不支持对象学习编程的语言中,我们会做类似的事情:

lower(x)

Python 有函数和对象重编程,这就是为什么有x.lower()和:

len(x)

# 2

函数或“消息”的选择取决于库设计者,但是lower仅对字符串有意义,因此将它与str的定义组合起来是有意义的。

然而,在实现方面,x.lower()实际上实现为str.lower(x)其中str是字符串的类定义。 电脑处理器了解函数调用; 他们不理解对象,所以我们在 Python 解释器本身中执行了这个翻译。

包 VS 对象成员

让我们直截了当。点.运算符在 Python 中重载,表示包成员和对象成员访问。你已经熟悉了这个:

import numpy as np
np.array([1,2,3])

# array([1, 2, 3])
import math
math.log(3000)

# 8.006367567650246

阅读代码时,这是一个常见的混淆点。 当我们看到a.f()时,我们不知道函数f是由a标识的包的成员,还是由a引用的对象的成员。

wordsim项目中,你定义了一个名为wordsim.py的文件,然后我的test_wordsim.py文件执行from wordsim import *来导入wordsim.py中的所有函数。

练习

在下文中,将标识符(单词)标识为包或函数或字段:

  1. np.log(3)
  2. np.linalg.norm(v)
  3. from sklearn.ensemble import RandomForestRegressor
  4. pd.read_csv("foo.csv")
  5. pd.read_csv
  6. 'hi'.lower()
  7. 'hi'.lower
  8. df_train.columns
  9. np.pi
  10. img = img.convert("L")

现在,确定子表达式的数据类型,并将标识符(单词)标识为包或函数或字段:

  1. df["saledate"].dt.year
  2. df_train.isnull().any().head(60)

字段 VS 方法

对象具有函数,我们将其称为方法,以将它们与不与对象关联的函数区分开来。 对象也有变量,我们称之为字段实例变量

字段是对象的状态。 方法是对象的行为

我们一直在使用字段,例如df.columns,它获取数据帧中的列名的列表。

import datetime
now = datetime.date.today()
print( type(now) )
print( now.year ) # access field year
print( now.month )

'''
<class 'datetime.date'>
2018
8
'''

如果尝试在没有括号的情况下访问对象函数,则表达式将计算为函数对象本身而不是调用它:

s='hi'
s.title

# <function str.title>

简单的对象定义

类是多个对象的蓝图,通常称为实例。 类*封装了对象的状态和行为。

想象一下外星人在你家后院的土地上,并要求你描述一辆汽车。 您可能会描述其属性,例如轮子的数量及其功能,例如可以启动和停止。 这些是状态和行为。 通过定义它们,我们有效地定义了对象。 类名仅仅为实体命名。

按照惯例,类名称应该像“Point”一样大写。

作为对象替代品的元组

对象的字段是我们想要关联在一起的数据项。 例如,如果我想跟踪书名/作者,我可以使用元组列表:

from lolviz import *
books = [
    ('Gridlinked', 'Neal Asher'),
    ('Startide Rising', 'David Brin')
]

objviz(books)

svg

for b in books:
    print(f"{b[1]}: {b[0]}")
    
'''
Neal Asher: Gridlinked
David Brin: Startide Rising
'''
# Or, more fancy
for title, author in books:
    print(f"{author}: {title}")
    
'''
Neal Asher: Gridlinked
David Brin: Startide Rising
'''

为了在两种情况下访问元组的元素,我们必须跟踪我们头脑中的顺序。 换句话说,我们必须访问元组元素,就像它们是列表元素一样。

形式对象

更好的方法是正式声明,作者和标题数据元素应该封装到称为书籍的单个实体中。我认为 Python 的规范非常古怪,但它非常灵活。 例如,我们可以定义一个没有方法没有字段的对象,但是可以使用赋值语句动态添加字段:

class Book:
    pass

b = Book()
print(b)
b.title = 'Gridlinked'
b.author = 'Neal Asher'
print(b.title, b.author)
objviz(b)


'''
<__main__.Book object at 0x115c51fd0>
Gridlinked Neal Asher
'''

svg

但这并不能让我们定义与该对象相关的方法(很容易)。让我们看看我们的第一个真正的类定义,它包含一个名为构造器的函数。

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author
        self.chapters = []

构造器通常根据参数设置初始和默认字段值。

在对象中定义的所有方法,函数必须有一个名为self的显式第一个参数。 这是正在考虑的对象。

然后我们可以使用实例创建语法Book(..., ...)来创建一列Book类的书籍对象或实例:

books = [
    Book('Gridlinked', 'Neal Asher'),
    Book(title='David Brin', author='Startide Rising')
]
objviz(books)

svg

for b in books:
    print(f"{b.author}: {b.title}") # access fields
    
'''
Neal Asher: Gridlinked
Startide Rising: David Brin
'''

请注意,我们不会将self参数传递给构造函数。 它在调用一侧隐藏,但在定义一侧出现!

顽皮的行为

还要注意我们一直在使用构造函数设置对象的字段,Python 以其无限的灵活性允许你做非常顽皮的事情,比如在任意对象上设置字段:

class Foo:
    pass # just says "empty"

x = Foo()
x.foo = 3

即使类本身没有定义foo,也不会出错!

您甚至可以动态添加方法

定义方法

如果您尝试打印一本书,您将只看到类型信息和物理内存地址:

print(books[0])

# <__main__.Book object at 0x115c51eb8>
class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author
        
    def __str__(self): # called when conversion to string needed like print
        return f"Book({self.title}, {self.author})"
    
    def __repr__(self): # called in interactive mode
        return self.__str__() # call the string
    
books = [
    Book('Gridlinked', 'Neal Asher'),
    Book('Startide Rising', 'David Brin')
]
print(books[0]) # calls __str__()
books[0]        # calls __repr__()

'''
Book(Gridlinked, Neal Asher)

Book(Gridlinked, Neal Asher)
'''

确保使用self.x来引用字段x,否则你在方法中创建一个局部变量:

class Foo:
    def __init__(self):
        self.x = 0
    def foo(self):
        x = 3 # WARNING: does not alter the field! should be self.x

让我们创建另一种设置销售图书数量的方法。

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author
        self.sold = 0 # set default
        
    def sell(self, n):
        self.sold += n
        
    def __str__(self): # called when conversion to string needed like print
        return f"Book({self.title}, {self.author}, sold={self.sold})"
    
    def __repr__(self): # called in interactive mode
        return self.__str__() # call the string
b = Book('Gridlinked', 'Neal Asher')
print(b)
b.sell(100) # Book.sell(b, 100)
print(b)

'''
Book(Gridlinked, Neal Asher, sold=0)
Book(Gridlinked, Neal Asher, sold=100)
'''

注意:在方法定义中,我们调用同一对象上的其他方法,使用self.foo(...)调用方法foo

理解方法和函数的关键

b.sell(100)方法调用由 Python 解释器翻译并执行为函数调用Book.sell(b, 100)b变成参数self,所以sell()函数正在更新book b

为什么我们更喜欢b.sell(100)而不是Book.sell(b, 100):我们不仅仅在函数,并且在对象之间来回传递消息。 我们说dog.bark(),不是bark(dog),或我们说ball.inflate(),而不是inflate(ball)

练习

真实世界的对象包含......和......

软件对象的状态存储在......

软件对象的行为通过......公开

软件对象的蓝图称为...

练习

定义一个名为Point的类,它有一个构造函数,接受xy坐标并使它们成为类的字段。

定义方法distance(q),它接受一个Point并返回从selfq的欧几里德距离。

使用这个来测试:

p = Point(3,4)
q = Point(5,6)
print(p.distance(q))

添加方法__str__,以便print(q)打印出类似(3, 4)的东西。

答案

import numpy as np

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def distance(self, other):
        return np.sqrt( (self.x - other.x)**2 + (self.y - other.y)**2 )
    
    def __str__(self):
        return f"({self.x},{self.y})"
p = Point(3,4)
q = Point(5,6)
print(p, q)
print(p.distance(q))

'''
(3,4) (5,6)
2.8284271247461903
'''

继承

定义一些与我们已经理解的东西相关的新东西,通常要容易得多。 在编程中也是如此。 让我们从一个帐户对象开始:

class Account:
    def __init__(self, starting):
        self.balance = starting

    def add(self, value):
        self.balance += value

    def total(self):
        return self.balance
a = Account(100.0)
a.add(15)
a.total()

# 115.0
objviz(a)

svg

继承的行为类似于import,或从另一个类导入操作到新类。 (请注意,这不是真的,但我们可以将其视为包含,对于我们目的。

如果我们不指定超类,则类object是隐式超类。 该类称为类层次结构的根,并定义了许多标准方法:

x = object() # yes, we can make a generic object
print(dir(x))

# ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

我们可以定义一个有息账户,因为它与普通账户不同:

class InterestingAccount(Account): # derive from super class to get subclass
    def __init__(self, starting, rate):
        self.balance = starting # super().__init__(starting)
        self.rate = rate
    def total(self): # OVERRIDE method
        return self.balance + self.balance * self.rate

b = InterestingAccount(100.0, 0.15)
b.add(15)
b.total()
objviz(b)

svg

要点是我们可以使用add()而不必在InterestingAccount中重新定义它,而InterestingAccount也可以覆盖帐户的total()。 我们已经复用覆盖以前的功能。您可以将超类视为定义一些初始函数,我们可以在子类中重用或覆盖他们。

我们还可以通过添加不在超类中的方法来扩展功能。

class InterestingAccount(Account): # derive from super class to get subclass
    def __init__(self, starting, rate):
        super().__init__(starting) # does self.balance = starting above
        self.rate = rate

    def total(self): # OVERRIDE method
        return self.balance + self.balance * self.rate
    
    def profit(self):
        return self.balance * self.rate
b = InterestingAccount(100.0, 0.15)
b.add(15)
b.profit()

# 17.25
a = Account(100.0)
b = InterestingAccount(100.0, 0.15)
print(type(a))
print(type(b))

'''
<class '__main__.Account'>
<class '__main__.InterestingAccount'>
'''

类的定义实际上本身是对象,您可以使用任何对象的秘密字段访问它们:

print(b.__class__)
print(b.__class__.__base__)

'''
<class '__main__.InterestingAccount'>
<class '__main__.Account'>
'''

练习

  1. 什么是类?
  2. 类和实例之间有什么区别?
  3. 使用不带参数的构造函数定义类Foo的新实例。
  4. 访问对象字段的语法是什么?
  5. 方法与函数有何不同?
  6. __init__方法有什么作用?
  7. 给定类EmployeeManager,哪个是子类或者超类?
  8. 子类中的方法可以调用超类中定义的方法吗?
  9. 如何覆盖从超类继承的方法?

动态调度(高级)

当你调用b.add(15)时,Python 在bInterestingAccount)的对象定义中查找函数add。 因为我们从超类继承了该方法,所以子类知道它。 当我们调用b.total()时,Python再次查找InterestingAccount中的方法并找到一个重写方法。 这就是为什么b.total()不会调用Account版本。

这种行为是可取的,但起初非常混乱。 下面是一个实例,其中我添加了一个__str__方法给超类:

class Account:
    def __init__(self, starting):
        self.balance = starting

    def add(self, value):
        self.balance += value

    def total(self):
        return self.balance
    
    def __str__(self):
        return f"Balance {self.total()}" # can call 2 different functions
    
class InterestingAccount(Account): # derive from super class to get subclass
    def __init__(self, starting, rate):
        self.balance = starting
        self.rate = rate

    def total(self): # OVERRIDE method
        return self.balance + self.balance * self.rate
    
    def profit(self):
        return self.balance * self.rate

微妙的部分是Account中的__str__调用Account.total()InterestingAccount.total(),具体取决于self的类型:

a = Account(100.0)
b = InterestingAccount(100.0, 0.15)
print(a) # calls Account.total()
print(b) # calls InterestingAccount.total()

'''
Balance 100.0
Balance 115.0
'''

练习

定义一个继承自PointPoint3D

定义接受x, y, z值并设置字段的构造函数。 调用super().__init__(x, y)来调用超类的构造函数。

定义/覆盖distance(q),以便它处理 3D 字段值来返回距离。

使用这个来测试:

p = Point3D(3,4,9)
q = Point3D(5,6,10)
print(p.distance(q))

添加方法__str__,以便print(q)打印出类似(3, 4, 5)的东西。记住:

$dist(x,y) = \sqrt{(x_1-y_1)^2 + (x_2-y_2)^2 + (x_3-y_3)^2)}$

答案

import numpy as np

class Point3D(Point):
    def __init__(self, x, y, z):
        # reuse/refine super class constructor
        super().__init__(x,y)
        self.z = z
        
    def distance(self, other):
        return np.sqrt( (self.x - other.x)**2 +
                        (self.y - other.y)**2 +
                        (self.z - other.z)**2 )
    
    def __str__(self):
        return f"({self.x},{self.y},{self.z})"
p = Point3D(3,4,9)
q = Point3D(5,6,10)
print(p.distance(q))

# 3.0

理由和一般思想

因为猎人 - 收集者的思想将世界视为通过发送消息交互的对象集合,所以 OO 编程范例很好地映射到现实世界问题,我们试图通过计算机模拟它们。 此外,在使用我们的思维方式编程时,我们处于最佳状态。

通常,在编写软件时,我们会尝试将现实实体映射到编程结构中。 如果我们有了单词问题,名词通常会成为对象,而动词通常会成为这些对象中的方法。

因为我们可以指定不同类型的对象如何相似,所以我们可以定义新对象,因为它们与现有对象不同。 通过按类别/共性/相似性正确地关联类似的类,作为继承的副作用,代码重用就出现了。

非 OO 语言是不灵活/脆弱的,因为必须指定确切的变量类型。在 OO 语言中,多态是使用单个类型引用,来引用相似但不同类型的分组的能力。

转载于:https://my.oschina.net/wizardforcel/blog/3068332

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值