Python学习笔记 —— 对象

来源:互联网 发布:2017双十一交易数据 编辑:程序博客网 时间:2024/06/06 06:33

本文档根据《Python3 面向对象编程》第二章内容整理总结,侵权请联系

通过学习本节的内容,我们将学到:
- 在Python中如何创建类并实例化对象
- 如何给Python对象添加属性和行为
- 如何把类组织成包和模块
- 如何建议别人不要错误地使用我们的数据

本节目录:

    • 创建Python类
      • 添加属性
      • 让类实现一些功能
      • 对象的初始化
      • 解释你自己
    • 模块和包
      • 组织模块
    • 谁可以访问我的数据


创建Python类

我们知道Python是一门非常简洁的语言,在Python3中一个最简单的类是这样实现的

class MyFirstClass:    pass

如果我们调用print打印一个类的对象,它会告诉我们它是哪个类,以及在内存中的存放位置(尽管在Python中不常使用内存地址)

>>> a = MyFirstClass()>>> print(a)<__main__.MyFirstClass object at 0xb7b7faec>

添加属性

我们现在有一个基本的类,但是它毫无用处。它不包含任何数据,并且也不能做任何事情。在类的定义中,我们不必要做任何特殊的操作。我们可以通过点记法给一个实例化的对象赋予任意属性。

class Point:    passp1 = Point()p2 = Point()p1.x = 5p1.y = 4p2.x = 3p2.y = 6

上边这段代码创建了一个没有任何属性和行为的空类,然后创建了两个类的实例,并给每个实例赋予了一个x坐标和y坐标来标识一个二位空间的点。我们需要做的只是通过 <object>.<attribute>=<value> 这个语法来为属性赋值。这种方式有时候被称为点记法

让类实现一些功能

我们从一个reset的方法开始。

class Point():    def reset(self):        self.x = 0        self.y = 0p = Point()p.reset()

方法和普通的函数有一点不同,就是所有的方法都有一个必须的参数,通常被称为 self 。这个命名是约定俗称的,即使如此,我们还可以称它为 this 或者 Martha 都可以。

我们调用方法的时候,并不需要给它传入 self 参数。Python 自动帮我们完成了,它知道我们正在调用对象的一个方法,所以它自动把对象传给了这个方法。如果我们在定义类的方法的时候忘记包含 self 参数,Python 会返回错误消息 takes no arguments。当程序提示你缺少参数的时候,第一件事情就是去检查在方法定义时你是否忘了带self函数。给方法传递多个参数:

class Point:    def move(self, x, y):        self.x = x        self.y = y    def reset(self):        self.move(0, 0)

对象的初始化

大部分面向对象的编程语言都有一个叫构造函数的特殊方法,当对象被创建的时候会自动调用构造函数。在这点上Python和它们有一点不同,Python有一个构造函数和一个初始化函数。正常情况下,构造函数很少能被用到,除非你想做一些特别另类的操作,下面我们讨论初始化的方法。

除了有一个特殊的名字__init__以外,Python的初始方法和其他方法没有什么不同。开始和结尾的双下划线的意思是:这是一个特殊的方法,Python的解释器会特殊对待他

class Point:    def __init__(self, x, y):        self.move(x, y)    def move(self, x, y):        self.x = x        self.y = y    def reset(self):        self.move(0, 0)point = Point(3, 5)

如果我们构造一个point对象的时候没有包含合适的初始化参数,程序回报一个“没有足够参数”的错误,与之前我们收到的那个错误类似。如果不想让这两个参数是必须的,我们怎么办?

我们看可以通过不改变Python函数的语法,而通过提供函数默认值来实现,语法就是在每一个变量后边通过等号赋予参数默认值。如果创建对象时没有提供参数,那么就会使用默认参数值。

class Point:    def __init__(self, x=0, y=0):        self.move(x, y)

大多数情况下,我们会把初始化的语句放到 __init__ 函数里。但是,就像之前提到的一样,除了有初始化函数之后,Python还有一个构造函数 __new__,并且它只接收一个参数,就是这个将要构造的类本身,同时返回刚被创建的对象。它会在对象被构造之前调用,所以这里没有self参数。当设计复杂的元编程技术时,可能会用到。

解释你自己

Python是一门非常简单易读的编程语言,有些人可能认为它是self-documenting。当我们进行面向对象的编程时,清晰地总结每一个方法是做什么的,并把这些内容写成API文档是很重要的。最好的方法就是把文档写到代码里。

Python的 docstring 文档字符串提供了对这种文档方式的支持。在每一个类,函数,方法的开头,紧接着他们的定义(以冒号结尾的那行)可以有一行Python的标准字符串,这行字符串也要和下面的代码一样有缩进。docstring 使用单引号或者双引号标注的Python字符串即 ' '" "

文档字符串的惯例是一个多行字符串,它的首行以大写字母开始,句号结尾。第二行是空行,从第三行开始是详细的描述。强烈建议在你的函数中使用文档字符串时遵循这个惯例。

import mathclass Point:    'Represents a point in two-dimensional geometric coordinates'    def __init(self, x=0, y=0):        '''Initialize the position of a new point. The x and y            coordinates can be specified. If they are not, the point           defaults to the origin.'''        self.move(x, y)    def move(self, x, y):        "Move point to a new location in two-dimensional sapce."        self.x = x        self.y = y    def reset(self):        'Reset the point back to the geometric orgin.'        self.move(0, 0)    def calculate_distance(self, other_point):        """Calculate the distance from this point to a second point            passed as a parameter.        This function uses the Pythagorean Theorem to calculate the         distance between between the two points. The distance is         returned as a float."""        return math.sqrt(                    (self.x - other_point.x)**2 +                    (self.y - ohter_point.y)**2)

在Python的交互式解释器中输入 help(Point) 就可以看到文档。

模块和包

现在我们知道了如何创建,实例化类,是时候想一下要如何组织他们了。

对于小的程序来说,我们可以把所有的类都放到一个 Python 文件里,并且在文件的最后通过一些代码来使用他们。但是随着我们项目的发展,在我们定义的众多类中找到想要编辑的哪个类变得很困难。模块(Module)就这么产生了。模块是非常简单的 Python 文件。在小型程序里,单个 Python 文件就是一个模块,两个文件就是两个模块。如果你在同一个文件夹里有两个文件,两个文件就是两个模块。如果你在同一个文件夹里有两个文件,我们可以通过从这个模块加载一个类的方式来使用其他模块。

比如,如果我们在构建一个电子商务系统,我们可能会在数据库里存储大量数据。我们可以把所有关于数据库访问的类和函数放到一个单一文件里,可以取名为database.py。其他模块为了访问数据库,可以从模块里导入关于数据库访问的类。

import语句是用来导入模块或者从模块里导入特定的类或者函数。假设我们有一个叫database.py的模板,它包含一个叫Database的类,第二个模块叫products.py,他负责产品相关的查询。

import databasedb = database.Database()# 数据库搜索操作

在上边的代码中,我们把database模块导入到了products命名空间(在一个模块或者函数内部可以访问到的名称列表),这个时候任何在database这个模块里的类或者函数,都可以通过database.<something>这种记法来访问。或者我们可以用from...import语法来导入一个类:

form database import Databasedb = Database()# 数据库搜索操作

假设出于某种原因,products自己已经有了一个叫Database的类,并且我们不想让两个名字冲突,那么在products模块里头使用这个类的时候,我们可以重新命名它:

from database import Database as DBdb = DB()# 数据库搜索操作

我们也可以在同一行语句里导入多项。如果database模块同时包含一个叫Query的类,可以通过下面的语句导入这两个类:

from database import Database, Query

我们甚至可以用下面的语法从database模块里导入他所有的类和函数:

from database import *

但请务必不要这么使用。当我们在文件开始用from database import Database明确地导入Database类的时候,可以清楚地看到Database类是从哪里来的,我们可以在交互式解释器中导入这个模块,然后使用help(database.Database)命令来查询如何使用这个类。但如果我们用from database import *这种语法,代码维护会变成噩梦。

使用 import * 语法会把一些意想不到的对象导入到我们本地的命名空间。肯定的一点是,它会把这个模块里定义的所有类和函数导入进来,但是,这样也会把这个模块自己导入的任意类和模块导入到当前文件中。总而言之,不要使用这种语法导入其他模块。

组织模块

随着项目发展成越来越多模块的集合,我们可能会发现,还需要增加另外一层抽象,基于模块水平的某种层次结构。

一个是放到一个文件夹里的模块集合。包的名字就是文件夹的名字。我们需要做的只是告诉 Python 这个文件夹是一个包,并且把一个名为 __init__.py 的文件 (通常是空的) 放到这个文件夹里。如果我们忘了创建这个文件,就没法从这个文件夹里导入哪些模块。

在我们的工作目录里,吧我们的模块放到了一个叫 ecommerce 的包里,这个目录同样包含了一个 main.py 文件来启动程序。此外,在 ecommerce 包里再增加一个叫 payments 的包用来管理不同的付款方式。文件夹的层次结构看起来是这样的:

parent_directory/    main.py    ecommerce/        __init__.py        database.py        products.py        payments/            __init__.py            paypal.py            authorizenet.py

当在包之间导入模块或类的时候,我们需要注意语法。在 Python3 中,导入模块有两种方式:绝对导入和相对导入。


绝对导入

绝对导入需要指明这个模块,函数的完整路径,或者我们希望导入的路径。如果我们需要访问 products 模块里头的 Product 类,我们可以使用下面这些语法做绝对导入:

import ecommerce.productsproduct = ecommerce.products.Product()

或者

from ecommerce.products import Productproduct = Product()

或者

from ecommerce import productsproduct = products.Product()

import 语句使用点号作为分隔符来分隔包或者模块。

对于任意模块,这些语句都可以运行。我们可以在 main.py 里,在 database 模块里,或者任意一个 payment 模块里使用这样的语法实例化一个 Product 对象。

确实,只要这些包在 Python 里是可用的,就可以导入他们。比如,这些包还可以安装到 Python 的 site packages 文件夹里,或者可以通过自定义 PYTHONPATH 环境变量来动态地告诉 Python,如何搜索它即将要读入的包或者模板。

在命名冲突的情况下 (比如我需要访问两个名字都叫 products 但完全不同的模块,需要加以区分),我们需要使用 from ecommerce import products as xx 这种语法。


相对导入

如果当前我们在 products 模块下工作,想从隔壁的 database 模块里导入 Database 类,我们可以使用相对导入:

from .database import Database

database 前边的点号说明,使用当前包里的 database 模块。在这种情况下,当前的包指的是 ecommerce包。

我们可以通过使用更多的点来访问层级层次的更上层。假设我们有一个 ecommerce.contact 包,这个包有一个 email 模块,我们要把这个模块中的 send_email 函数导入到我们的 paypal 模块中,下面的导入语句是有效的:

from ..contact.email import send_email

在任何一个模块里,我们可以指定要访问的变量,类或者函数。这是一个很方便的方法,可以用来储存没有命名空间冲突的 (模块间的) 全局变量 。比如,我们已经把 Database 类导入到了不同的模块里,并且实例化了它,但是,在 database 模块里,有且只有一个全局的 database 对象会更有意义一些。此时的 database 模块看起来应该是这个样子的:

class Database:    # 具体的实现    passdatabase = Database()

这样,我们就可以使用任意一种讨论过的方法来访问 database 对象,例如:

from ecommerce.database import database

但是此处还有坑的,可以参考博客Python 中模块间全局变量的使用上的注意

上边这个类有个问题,就是在第一次导入这个模块的时候,就立即创建了 database 对象,通常对象的创建应该在程序启动的时候。事情总不是那么理想,因为数据库连接需要一段时间,这会减缓程序启动,或者也许得不到数据库连接信息。我们可以减缓 database 对象的创建,知道真正需要它的时候,通过调用 initialize_database 函数来创建模块级别的变量。

class Database:    # 数据库实现    passdatabase = Nonedef initialize_dataset():    global database    database = Database()

global 关键词告诉 Python,我们刚刚在 initialize_database 里定义了一个模块级别的 database 变量。如果我们没有指明这个变量是全局的,当该方法执行完之后, Python 在这个方法内部创建的这个 database 变量会被抛弃,剩下的那个模块级别的 database 变量,值不会变 (None)。

就像刚才例子中展示的一样,当导入模块的时候,模块里所有的代码会被立即执行。但是如果模块里的是一个方法或者函数,会创建这个函数,但是函数里的代码直到函数被调用的时候才会被执行。通常写一个程序的时候,我们想从一个程序里的一个模块导入一个函数或者类。但只要我们导入了它,这个模块里所有的代码都会被立即执行。当我们只想访问那个模块里的一些函数的时候,如果不小心,可能会把当前正在运行的程序终止掉。

为了解决这个问题,我们应该总把启动代码放到一个函数里(通常叫做 main 函数),并且只有当我们知道这是在执行脚本的时候,才回去执行这个函数,而不是在其他脚本导入我们代码的时候。但我们如何知道呢?

class UsefulClass:    '''This class might be useful to other modules.'''    passdef main():    '''creates a useful class and does something with    it for our module.'''    useful = UsefulClass()    print(useful)if __name__ == "__main__":    main()

每一个模块都有一个特殊的变量 __name__, 当导入这个模块的时候,这个变量指明了模块的名字。但是当这个模块直接通过 python module.py 执行的时候,就不会导入这个变量,而这时 __name__ 变量被赋予 __main__

方法出现在类里,类出现在模块里,模块出现在包里,一定是这样吗?

不是这样的,当然这确实是 Python 程序里的典型顺序,但并不是唯一可能的布局方式。类可以定义在任何地方。他们通常是在模块级别定义的,但是它们也可以在一个函数或者方法内部定义,像:

def format_string(string, formatter=None):    '''Format a string using the formatter object, which    is expected to have a format() method that accepts    a string.'''    class DefaultFormatter:        '''Format a string in title case.'''        def format(self, string):            return str(string).title()    if not formatter:        formatter = DefaultFormatter()    return formatter.format(string)hello_string = "hello world, how are you today?"print(" input: " + hello_string)print("output: " + format_string(hello_string))

输出:

 input: hello world, how are you today?output: Hello World, How Are You Today?

类似地,函数也可以定义在另一个函数里。总之,任何时候都可以执行任何 Python 语句。这种“内部”类或者函数是有用的,特别对于那些在模块级别不需要或者值得保留自己作用范围的项目,或者只有在一个单一方法里有意义的项目。

谁可以访问我的数据

大多数面向对象的编程语言都会有一个访问控制的概念,这个和抽象有关。对象里的某些属性和方法会被标记为“private”,意思是只有这个对象可以访问它们。另外一些会被标记为“protected”,意思是只有这个类和它的子类可以访问。剩下的会被标记为“public”,意思是允许任何其他对象访问它们。

但 Python 没有这样做。Python 不相信强制制定规则这种方式会让人严格遵守。相反,它提供了一个不强制的指南和最佳实践。在技术层面上,一个类里所有方法和属性都是公共可访问的。如果我们想建议某个方法不应该能被公共访问,我们可以通过在 docstring 里放一个提示来表明这个方法是仅供内部使用的。

按照惯例,我们可以给一个属性或者方法加一个下划线前缀,大部分 Python 程序员会把这个解释为:这是一个内部变量,在直接访问它之前请三思。但是如果别人认为访问这个变量能给他们的程序带来很大的帮助,那么什么也无法阻止他们去访问。

你还可以用另外一种方式,强烈建议外部对象不能访问某个属性或者方法,就是给它添加一个双下划线的前缀。这就是所谓的对问题的属性做 “名称改编”。基本的意思是,外部对象如果真的想访问的话,还是仍然可以调用这个方法的,但是你需要额外的工作,并且它是一个很强的指示器,指示你想到你的属性应该保持私有性。例如:

class SecretString:    '''A not-at-all secure way to store a secret string.'''    def __init__(self, plain_string, pass_phrase):        self.__plain_string = plain_string        self.__pass_phrase = pass_phrase    def decrypt(self, pass_phrase):        '''Only show the string if the pass_phrase is correct.'''        if pass_phrase == self.__pass_phrase:            return self.__plain_string        else:            return ''
>>> secret_string = SecretString("ACME: Top Secret", "antwerp")>>> print(secret_string.decrypt("antwerp"))ACME: Top Secret>>> print(secret_string.__plain_text)Traceback (most recent call last):File "<stdin>", line 1, in <module>AttributeError: 'SecretString' object has no attribute'__plain_text'

看起来它生效了,没有密码的话没有人可以访问我们的 plain_text 属性,所以它一定是安全的。不过,其实破译它是很容易的:

>>> print(secret_string._SecretString__Plain_string)ACME: Top Secret

Python 的 “名称改编” 起作用了。当我们使用双下划线开头定义一个属性时,这个属性会自动加上一个 _<classname> 的前缀。这时候这个类的方法在内部访问变量,它们没有被自动转换。当外部类想要访问它的时候,它们必须要自己做名称改编。大部分情况下,在 Python 代码中没有什么好的理由说一定要用 “名称改编” 的变量,这样可能产生副作用。比如,一个 “名称改编” 的变量会对子类起作用。

最后,我们可以直接从包里导入变量,这和从包里导入模块截然不同。在之前的例子中,我们有一个 ecommerce 包,它包含两个模块,一个叫 database.py,一个叫 products.pydatabase 模块里有一个 db 变量,这个变量在很多地方都会被访问。如果我们用 import ecommerce.db 代替 import ecommerce.database.db 这样会不会更方便一些呢?

还记得 __init__.py 文件定义目录为包吗?只要我们愿意,这个文件可以包含任意变量或类的声明,而且他们会作为这个包的一部分被我们使用。在例子中,如果 ecommerce/__init__.py 包含这么一行:

from .database import db

这样我们就可以用下面的导入语句,在 main.py 或者其他文件中访问 db 这个属性了:

from ecommerce import db

如果把 __init__.py 看作 ecommerce.py, ecommerce 可以被当作一个模块而不是一个包,这样可能会有助于理解。当我们想把一个模块的代码分割成一个包含多个模块的包时,__init__.py 十分有用。__init__.py 可以继续当作程序的切入点(外部接口不变),但内部的结构可以被重新组织。

原创粉丝点击