第四章 机器学习

面向对象编程

在我们编写的所有程序中,我们已经围绕函数设计了我们的程序,即操作数据的语句块。这称为面向过程的编程方式。还有另一种组织程序的方法,即组合数据和功能并将其包装在称为对象的内容中。这称为面向对象编程范例。大多数情况下,您可以使用过程编程,但在编写大型程序或遇到更适合此方法的问题时,可以使用面向对象的编程技术。

类和对象是面向对象编程的两个主要方面。一创建一个新的类型,其中对象实例的类。类比是你可以拥有类型的int变量,这些变量转换成说存储整数的变量是变量,它们是类的实例(对象)int

静态语言程序员注意事项

请注意,即使整数也被视为对象(int类)。这与C ++和Java(1.5版之前)不同,其中整数是原始本机类型。

有关help(int)课程的详细信息,请参阅。

C#和Java 1.5程序员会发现这类似于装箱和拆箱的概念。

对象可以使用属于对象的普通变量来存储数据。属于对象或类的变量称为字段。通过使用属于类的函数,对象也可以具有功能。这些函数称为类的方法。这个术语很重要,因为它有助于我们区分独立的函数和变量以及属于类或对象的函数和变量。总的来说,字段和方法可以称为该类的属性

字段有两种类型 - 它们可以属于类的每个实例/对象,也可以属于类本身。它们分别称为实例变量类变量

使用class关键字创建一个类。该类的字段和方法列在缩进块中。

self

类方法与普通函数只有一个特定的区别 - 它们必须有一个额外的名字,必须添加到参数列表的开头,但是当你调用方法时,你不会给这个参数赋值,Python会提供它。这个特定的变量引用了对象本身,按照惯例,它被赋予了名称self

虽然,您可以为此参数指定任何名称,但强烈建议您使用该名称self- 任何其他名称肯定是不受欢迎的。使用标准名称有许多优点 - 程序的任何读者都会立即识别它,甚至专业的IDE(集成开发环境)也可以帮助您使用self

C ++ / Java / C#程序员注意事项

self在Python等同于this用C ++指针和this在Java和C#参考。

您一定想知道Python如何为其提供价值self以及为什么您不需要为其提供值。一个例子可以说明这一点。假设你有一个被调用的类,MyClass并且调用了这个类的实例myobject。当您调用此对象的方法时myobject.method(arg1, arg2),Python会自动将其转换为MyClass.method(myobject, arg1, arg2)- 这就是所有特殊self内容。

这也意味着如果你有一个不带参数的方法,那么你仍然必须有一个参数 - self

以下示例中显示了最简单的类(另存为oop_simplestclass.py)。

class Person:
    pass  # An empty block

p = Person()
print(p)

输出:

$ python oop_simplestclass.py
<__main__.Person instance at 0x10171f518>

这个怎么运作

我们使用class语句和类的名称创建一个新类。接下来是一个缩进的语句块,它构成了类的主体。在这种情况下,我们有一个空块,使用该pass语句表示。

接下来,我们使用类的名称后跟一对括号创建此类的对象/实例。(我们将在下一节中了解有关实例化的更多信息)。对于我们的验证,我们只需打印即可确认变量的类型。它告诉我们模块中有一个Person类的实例__main__

请注意,还会打印存储对象的计算机内存的地址。地址在您的计算机上具有不同的值,因为Python可以将对象存储在找到空间的任何位置。

方法

我们已经讨论过类/对象可以像函数一样使用方法,除了我们有一个额外的self变量。我们现在看一个例子(另存为oop_method.py)。

class Person:
    def say_hi(self):
        print('Hello, how are you?')

p = Person()
p.say_hi()
# The previous 2 lines can also be written as
# Person().say_hi()

输出:

$ python oop_method.py
Hello, how are you?

这个怎么运作

在这里,我们看到了self实际行动。请注意,该say_hi方法不带参数,但仍具有self函数定义中的参数。

__init__方法

有许多方法名称在Python类中具有特殊意义。我们现在将看到该__init__方法的重要性。

__init__一旦实例化(即创建)类的对象,就运行该方法。该方法对于您想要对对象进行任何初始化(即将初始值传递给对象)很有用。请注意,名称的开头和结尾都有双重下划线。

示例(另存为oop_init.py):

class Person:
    def __init__(self, name):
        self.name = name

    def say_hi(self):
        print('Hello, my name is', self.name)

p = Person('Swaroop')
p.say_hi()
# The previous 2 lines can also be written as
# Person('Swaroop').say_hi()

输出:

$ python oop_init.py
Hello, my name is Swaroop

这个怎么运作

在这里,我们将__init__方法定义为采用参数name(与通常一样self)。在这里,我们只创建一个名为的新字段name。请注意,这些是两个不同的变量,即使它们都被称为“名称”。没有问题,因为虚线表示法self.name意味着存在称为“名称”的东西,它是被称为“自我”的对象的一部分而另一个name是局部变量。由于我们明确指出了我们所指的名称,因此没有混淆。

在创建类的新实例时pPerson我们使用类名,然后是括号中的参数:p = Person('Swaroop')。

我们没有明确地调用该__init__方法。这是这种方法的特殊意义。

现在,我们可以self.name在方法中演示的say_hi方法中使用该字段。

类和对象变量

我们已经讨论了类和对象(即方法)的功能部分,现在让我们了解数据部分。数据部分,即字段,只不过是绑定到类和对象的名称空间的普通变量。这意味着这些名称仅在这些类和对象的上下文中有效。这就是为什么它们被称为名称空间

有两种类型的字段 - 类变量和对象变量,它们根据类或对象是否分别拥有变量进行分类。

类变量是共享的 - 它们可以被该类的所有实例访问。只有一个类变量的副本,当任何一个对象对类变量进行更改时,所有其他实例都会看到该更改。

对象变量由类的每个单独对象/实例拥有。在这种情况下,每个对象都有自己的字段副本,即它们不共享,并且在不同的实例中不以任何方式与字段相关。一个例子将使这个易于理解(另存为oop_objvar.py):

class Robot:
    """Represents a robot, with a name."""

    # A class variable, counting the number of robots
    population = 0

    def __init__(self, name):
        """Initializes the data."""
        self.name = name
        print("(Initializing {})".format(self.name))

        # When this person is created, the robot
        # adds to the population
        Robot.population += 1

    def die(self):
        """I am dying."""
        print("{} is being destroyed!".format(self.name))

        Robot.population -= 1

        if Robot.population == 0:
            print("{} was the last one.".format(self.name))
        else:
            print("There are still {:d} robots working.".format(
                Robot.population))

    def say_hi(self):
        """Greeting by the robot.

        Yeah, they can do that."""
        print("Greetings, my masters call me {}.".format(self.name))

    @classmethod
    def how_many(cls):
        """Prints the current population."""
        print("We have {:d} robots.".format(cls.population))


droid1 = Robot("R2-D2")
droid1.say_hi()
Robot.how_many()

droid2 = Robot("C-3PO")
droid2.say_hi()
Robot.how_many()

print("\nRobots can do some work here.\n")

print("Robots have finished their work. So let's destroy them.")
droid1.die()
droid2.die()

Robot.how_many()

输出:

$ python oop_objvar.py
(Initializing R2-D2)
Greetings, my masters call me R2-D2.
We have 1 robots.
(Initializing C-3PO)
Greetings, my masters call me C-3PO.
We have 2 robots.

Robots can do some work here.

Robots have finished their work. So let's destroy them.
R2-D2 is being destroyed!
There are still 1 robots working.
C-3PO is being destroyed!
C-3PO was the last one.
We have 0 robots.

这个怎么运作

这是一个很长的例子,但有助于演示类和对象变量的本质。这里,population属于Robot类,因此是一个类变量。的name变量属于对象(它是使用分配self),并且因此是一个对象的变量。

因此,我们将population类变量称为Robot.population而不是self.population。我们在该对象的方法中name使用self.name符号来引用对象变量。记住类和对象变量之间的这种简单区别。另请注意,与类变量同名的对象变量将隐藏类变量!

而不是Robot.population,我们也可以使用,self.__class__.population因为每个对象都通过self.__class__属性引用它的类。

how_many实际上是属于类,而不是到对象的方法。这意味着我们可以将其定义为a classmethod或a,staticmethod具体取决于我们是否需要知道我们属于哪个类。由于我们引用了一个类变量,让我们使用classmethod

我们已经how_many使用装饰器将该方法标记为类方法。

装饰器可以被想象为调用包装函数的快捷方式(即一个“包裹”另一个函数的函数,以便它可以在内部函数之前或之后执行某些操作),因此应用@classmethod装饰器与调用相同:

how_many = classmethod(how_many)

注意该__init__方法用于使用Robot名称初始化实例。在这种方法中,我们将population计数增加1,因为我们还增加了一个机器人。还要注意,self.name每个对象的值都是特定的,表示对象变量的性质。

请记住,您必须self 使用相同对象的变量和方法。这称为属性引用

在这个程序中,我们还看到了对类和方法使用docstrings。我们可以在运行时使用Robot.__doc__docstring和docstring方法访问类docstringRobot.say_hi.__doc__

在该die方法中,我们简单地将Robot.population计数减少1。

所有班级成员都是公开的。一个例外:如果您使用带有双下划线前缀的名称的数据成员,例如__privatevar,Python使用名称修改来有效地使其成为私有变量。

因此,遵循的惯例是,仅在类或对象中使用的任何变量应以下划线开头,并且所有其他名称都是公共的,并且可以由其他类/对象使用。请记住,这只是一个约定,并不是由Python强制执行的(双下划线前缀除外)。

C ++ / Java / C#程序员注意事项

所有类成员(包括数据成员)都是公共的,所有方法都是Python 中的虚拟方法。

遗产

面向对象编程的一个主要好处是重用代码,其中一种方法是通过继承机制实现的。可以最好地将继承想象为实现之间的类型和子类型关系。

假设您想编写一个程序,该程序必须跟踪大学中的老师和学生。它们有一些共同的特征,如姓名,年龄和地址。他们还具有特定的特征,如薪水,教师课程和假期,以及学生的分数和费用。

您可以为每种类型创建两个独立的类并处理它们,但添加新的共同特征意味着添加到这两个独立的类。这很快变得笨拙。

更好的方法是创建一个公共类SchoolMember,然后让教师和学生类继承自这个类,即它们将成为这种类型(类)的子类型,然后我们可以为这些子类型添加特定的特征。

这种方法有许多优点。如果我们添加/更改任何功能SchoolMember,它也会自动反映在子类型中。例如,您只需将其添加到SchoolMember课程,即可为教师和学生添加新的ID卡字段。但是,子类型的更改不会影响其他子类型。另一个优点是,您可以将教师或学生对象称为SchoolMember对象,这在某些情况下可能很有用,例如计算学校成员的数量。这称为多态,其中子类型可以在期望父类型的任何情况下被替换,即该对象可以被视为父类的实例。

还要注意我们重用了父类的代码,我们不需要在不同的类中重复它,就像我们使用独立类时一样。

SchoolMember这种情况下的类称为基类超类。在TeacherStudent类被称为派生类子类

我们现在将此示例视为一个程序(另存为oop_subclass.py):

class SchoolMember:
    '''Represents any school member.'''
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print('(Initialized SchoolMember: {})'.format(self.name))

    def tell(self):
        '''Tell my details.'''
        print('Name:"{}" Age:"{}"'.format(self.name, self.age), end=" ")


class Teacher(SchoolMember):
    '''Represents a teacher.'''
    def __init__(self, name, age, salary):
        SchoolMember.__init__(self, name, age)
        self.salary = salary
        print('(Initialized Teacher: {})'.format(self.name))

    def tell(self):
        SchoolMember.tell(self)
        print('Salary: "{:d}"'.format(self.salary))


class Student(SchoolMember):
    '''Represents a student.'''
    def __init__(self, name, age, marks):
        SchoolMember.__init__(self, name, age)
        self.marks = marks
        print('(Initialized Student: {})'.format(self.name))

    def tell(self):
        SchoolMember.tell(self)
        print('Marks: "{:d}"'.format(self.marks))

t = Teacher('Mrs. Shrividya', 40, 30000)
s = Student('Swaroop', 25, 75)

# prints a blank line
print()

members = [t, s]
for member in members:
    # Works for both Teachers and Students
    member.tell()

输出:

$ python oop_subclass.py
(Initialized SchoolMember: Mrs. Shrividya)
(Initialized Teacher: Mrs. Shrividya)
(Initialized SchoolMember: Swaroop)
(Initialized Student: Swaroop)

Name:"Mrs. Shrividya" Age:"40" Salary: "30000"
Name:"Swaroop" Age:"25" Marks: "75"

这个怎么运作

要使用继承,我们在类定义中的类名后面的元组中指定基类名(例如,class Teacher(SchoolMember))。接下来,我们观察到__init__使用self 变量显式调用基类的方法, 以便我们可以初始化子类中实例的基类部分。这是remember-因为我们正在定义一个非常重要 __init__ 的方法TeacherStudent 子类,Python中不会自动调用基类的构造函数 SchoolMember,你必须自己显式调用它。

相反,如果我们没有__init__ 在子类中定义 方法,Python将自动调用基类的构造函数。

虽然我们可以把实例TeacherStudent实例,我们会SchoolMember和访问tell的方法,SchoolMember只需输入Teacher.tell或者Student.tell,我们代替另一种定义tell在每个子类中的方法(使用tell方法SchoolMember的一部分)来定制它为子类。因为我们已经这样做了,所以当我们编写Teacher.tellPython时,使用该tell子类与超类的方法。但是,如果我们tell在子类中没有方法,Python会使用tell超类中的方法。Python总是首先在实际的子类类型中开始寻找方法,如果它没有找到任何东西,它会开始按照元组中指定的顺序逐个查看子类的基类中的方法(这里我们只是在类定义中有1个基类,但你可以有多个基类。

关于术语的注释 - 如果继承元组中列出了多个类,则称为多重继承

end参数print在超类的tell()方法中用于打印一行,并允许下一个打印在同一行继续。这是一种在print打印\n结束时不打印(换行符号)的技巧。

摘要

我们现在已经探索了类和对象的各个方面以及与之相关的各种术语。我们还看到了面向对象编程的好处和缺陷。Python是高度面向对象的,从长远来看,仔细理解这些概念将对您有所帮助。

接下来,我们将学习如何处理输入/输出以及如何在Python中访问文件。

Last updated