【二】Python对象

来源:互联网 发布:如何管理淘宝网店 编辑:程序博客网 时间:2024/06/06 18:43

创建Python对象

在Python3中创建一个类通常是这样的:

class MyClass:    pass

类的定义以关键字class开头,之后跟着类的名字,以冒号结尾。

Python代码风格指南“PEP8”建议类的名字应使用驼峰记法,即每个单词的首字母大写。

紧跟着是类的定义,换行缩进4个空格。这里写了pass是因为目前这个类不需要执行任何东西,但我们依然可以实例化这个类,在idle中执行这样的操作:

>>> a=MyClass()>>> b=MyClass()>>> a<__main__.MyClass object at 0x7fb69a076a20>>>> b<__main__.MyClass object at 0x7fb69a0769e8>

这里打印的内容告我们:a.b分别是MyClass类的对象,内存地是多少。


添加属性

我们创建了一个基本的类,但它没有任何行为,也没有数据。要添加属性可以通过点记法个一个实例化的对象赋予任意属性:

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

run

5 43 6

这段代码创建了一个没有任何数据和行为的空Point类,然后实例化了p1,p2,给两个实例对象赋x,y的坐标值,并打印。

点记法的格式是这样的:object.attribute=value,用这个规则来为对象赋值。value可以是Python内置的数据类型、其他对象、一个函数、一个类。


让类做一些事情

给Point类添加一些内容,让它有点“意思”。可以创建一个叫reset的方法,这个方法用来把点移动到原点,并且这个方法不用任何参数(实际上是没有多余的参数,后面会讲到)。

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

结果不用说

0 0 

方法和函数的相同点
Python中方法的定义格式和函数相同:以关键字def开头,紧跟着一个空格和名称,名称后紧跟一对小括号,括号内填写这个函数的参数,然后以冒号结尾;下行缩进后写出这个函数的实现语句,它可以操作对象本身和任意传入的参数,前提是这个方法人为这个参数合法。
方法和函数的不同点
所有的方法都要有一个self参数(其实你可以命名为this,me,都可以,self是约定俗成的),这是对调用这个方法的对象的引用。
方法其实就是个函数,在类定义中的函数

你会注意到,p.reset()这个语句并没有写参数,Python知道我们要引用的self是p,即点记法的对象,所以程序自动把p传给了self。
这并不绝对。
还有一种方法可以带参数调用:

p=Point()Point.reset(p)print(p.x,p.y)

如何给方法传递多个参数?让我们添加一个新方法,这个方法允许我们把点移动到任意位置,不仅仅是移动到原点。

class Point:    def move(self,x,y):        self.x=x        self.y=y    def reset(self):        self.move(0,0)p1=Point()p2=Point()p1.reset()p2.move(5,0)print(p1.x,p1.y)print(p2.x,p2.y)

结果是

0 0 5 0

在这个例子中,move方法就有多个参数被传入,然后reset实现是调用move,相当于move的特殊情况。


对象初始化

如果不去清晰给出参数,我们将不会得到一个有用的点:

>>> point=Point()>>> point.x=10>>> #不给它y坐标>>> print(point.x,point.y)Traceback (most recent call last):  File "<pyshell#34>", line 1, in <module>    print(point.x,point.y)AttributeError: 'Point' object has no attribute 'y'

报错也告诉我们了,没有y属性。每次都要输入x,y,很不方便。所以,我们要使用初始化方法,来简化操作。

除了名字比较特殊__init__外,和普通的方法没有什么区别。双下划綫意味着“这是一个特殊方法,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(2,3)>>> print(point.x,point.y)2 3

现在就可以方便的指定坐标值了。但是我处于某些需要,创建的点都在原点上,该怎样简单一些呢?在初始化方法中设置默认参数就行了。

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

现在所有点,不带参数时将会产生在原点。


解释你的作品

当你文思泉涌写了一个精美绝伦的类时,你的小伙伴居然没看懂,这就尴尬了。为了方便其他人参考阅读你的精彩作品,你可以在代码阶段写入一些文字,解释每个对象是什么,每个方法是干什么的。
Python的docstring提供了对这种文档的支持,在每个类、函数、方法的定义阶段,可以添加一行多行注释文字,用来解释你这个东东是个什么玩意儿(注意要保持缩进状态)。
docstring应清晰准确地总结出它所描述的类或对象的用途,解释任何用法不那么明显的参数,并且也包含如何使用这些api的简单例子。任何一个api使用者应该知道的注意事项都应写在此处。
把上面的例子加入docstring:

class Point:    '''Creating some points with arguments or default methods'''    def __init__(self,x=0,y=0):        '''initialization of the point, by default x=0,y=0'''        self.move(x,y)    def move(self,x,y):        '''move method'''        self.x=x        self.y=y    def reset(self):        '''reset method'''        self.move(0,0)>>>#类定义完毕>>> help(Point)Help on class Point in module __main__:class Point(builtins.object) |  Creating some points with arguments or default methods |   |  Methods defined here: |   |  __init__(self, x=0, y=0) |      initialization of the point, by default x=0,y=0 |   |  move(self, x, y) |      move method |   |  reset(self) |      reset method

看,高大上的说明就有了。这就是docstring的用处。


模块和包

模块module是为了方便大型项目的类管理。在小程序里,一个文件就是一个模块,两个文件就是两个模块。

假设建立一个电商平台,把所有关于数据库查询的类和函数放在一个模块中(database.py)其他模块(客户、产品清单等)为了访问数据库,可以从这个模块里调用相应的类和函数。

import语句是用来导入模块或者从模块导入特定的类、函数之用:

>>>import math>>>math.log2(2)>>>1.0

继续电商的例子,有一个叫database.py的模块,包含一个Database的类,另一个模块叫做product.py模块,负责产品相关查询,此时不用想太多,简化一下:product.py需要在database.py里实例化一个Database类,然后就可以在数据中进行产品相关查询。有两种方法来实现:

import databasedb=database.Database()

这里,把database模块导入了product命名空间(一个模块或者函数内部可以访问到的名称列表),这个时候任何在database模块里的类和函数都可以通过database.something来访问。下面的方法也可以调用模块:

from database import Databasedb=Database()

两种方法都可以方便的调用模块。

假如product模块中自己已经有一个叫Database()的类,但是我们必须要调用database模块中的Database()类,重名了,怎么办?那么在导入模块的时候可以给他一个小名:

from database import Database as DBdb=DB()

假如我们要导入多个类,可以写在一起:

from database import Database, Query

注意import database 和 from database import *(慎用
很明显,后者把database 的所有自带的和自己定义的类和函数都导入了命名空间;而前者看似导入了整个模块,其实只是提供了一个访问模块的接口,在实际使用database.somethin之前,对命名空间影响不大。


组织模块

随着项目发展为越来越多的模块的集合,我们会发现,需要增加另外一层抽象,基于模块水品的某种层次模块。但是,一个文件就是一个模块,只能是一个模块。
但是,文件可以放在文件夹里,模块也是。一个包(package)就是放到一个文件夹里所有文件的集合。包的名字就是文件夹的名字,我们要做的是把一个__init__.py的文件放到文件夹里。
在上面的例子中,我们把模块放到了一个叫ecommerce(电子商务)的包里,这个目录也包含一个main.py的文件来启动程序。此外,在ecommerce文件夹里新建一个payments的包用来管理不同的付款方式。文件层次看起来是这样的:
这里写图片描述
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的site packages文件夹里,或者可以通过自定义PYTHONPATH环境变量来动态地高速Python,如何搜索它即将要导入的包或者模块。

那么这么多的方法,我们该怎么选择呢?这取决于你的爱好和手头的应用程序:如果我想用的这个products模块里有几十个类和函数,一般是使用from ecommerce import droducts 导入模块的名字,然后通过products.Product来访问单一的类。如果仅需要products模块中的某几个类,就推荐使用from ecommerce.products import Product来导入这几个类。记住你可以做任何事来让你的代码更加优雅简洁

相对导入

其实,玩过Unix-like系统的都明白相对路径绝对路径的用法,Python也是一样的用法。比如:当前我们在products模块(其实上就是一个文件)下工作,想导入“隔壁”的database模块里的Database类,使用相对导入语法:from .database import Product,看到了吧,和系统命令一样的,点代表当前目录下,.database 就是当前目录下的database.py文件。
假设现在我正在编辑paypal.py文件,想导入products模块里的Product类,那么就这么操作:from ..products import Product 看,两个点,和系统的命令操作一模一样的。

模块导入二三事

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

class Database:    #数据库实现    passdatabase = Database

这样我们就可以使用任意一种方式来访问database对象:

from ecommerce.database import database

上面的方法有个问题,第一次导入这个模块的时候,就立即创建了database对象,通常这会减缓程序启动时间,所以我们应该用间接的方式创建这个对象,真正需要它的时候,通过调用initialize_datebase 函数来创建模块级别的变量:

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

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

这两个例子说明了一个东西:

  • 当导入模块时,模块里的所有代码都会被执行。但是模块里的方法或者函数只会创建其本身,其内的代码直到函数(方法)被调用才会执行。

but,有时候我们只想要某个模块内部的某个函数,可是导入这个模块时,模块内所有的外层(相对于方法、函数的定义语句为内层)代码都执行了一遍,如果不小心,我们正在运行的程序会被终止掉。为了解决这个问题,我们应该把启动代码放到一个一个函数里(通常叫做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__,当导入了这个模块的时候,这个变量指明了模块的名字,if判断失效;但是在命令行执行这个模块(文件)的时候,__name__变量就被赋值为__main__,此时if判断生效,即可执行被这个判断管理的函数。这样做的好处是:

  1. 防止你在文本中导入模块的时候牵扯到多余的代码;
  2. 可以让模块运行额外的代码,可以起到模块测试之用。

方法出现在类里,类出现在模块里,模块出现在包里,所有的代码都是这样的吗?

非也。请看例子:

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?

format_string函数接收一个字符串和一个可选的格式化对象作为参数,然后执行格式化字符串操作。如果没有提供格式和方法,函数会自己创建一个格式化方法,作为一个本地的类并实例化它。既然是在函数内部创建的,这个类就不能放访问这个函数外面的任何地方。类似的,函数也可以定义在另一个函数里面。总之,任何时候都可以执行任何Python语句。这种“内部”类或函数都是有用的,特别对于那些在模块级别不需要或值得保留自己作用范围的“一次性”项目,或者只有在一个单一方法里有意义的项目。

访问控制

大多数面向对象编程语言都有一个“访问控制”的概念。这个和抽象有关。
主要有三个类型:
1. 私有(private):只有这个对象可以访问。
2. 受保护的(protected):只有这个类和它的子类可以访问。
3. 公共的(public):允许任何其他对象访问。

Python不会直接限制访问,但是有其独特的表示方法。在技术层面上,一个类所有的方法和属性都是可以公共访问的。如果我们建议某个方法不应该被公共访问,可以通过在docstring里放一个提示来说明这个方法是否是内部使用的,或者解释面向公共的API如何工作。

我们也可以给一个属性或者方法加一个下划线前缀,表明“这是一个内部变量,在直接访问他们前请三思”
例如:

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("top secret","123")>>> print(secret_string.decrypt("123"))top secret>>> print(secret_string.__plain_string)Traceback (most recent call last):  File "<pyshell#4>", line 1, in <module>    print(secret_string.__plain_string)AttributeError: 'SecretString' object has no attribute '__plain_string'

看起来在没有密码的情况下,是不能访问plain_string属性的,但是它真的不能访问吗?

>>> print(secret_string._SecretString__plain_string)top secret

这样,我们的密码就被破解了。当我们使用双下划线开头定义一个属性时,这个属性会自动加上一个_<calssname>的前缀。这是这个类的方法在内部访问,就不用完整输入_<calssname>,不会被自动转换;当外部类想要访问的时候,就要完整输入_<calssname>。所以名称改编不能保障隐私,只能强烈建议要保护隐私。这是一个约定俗成的规矩。

__init__.py的利用

前面就提到了__init__.py这个文件可以是空的,作用是放在某个文件夹下使这个文件夹成为包可以导入调用,那么它一定要空吗?

当然不是!我们前面所说的都是从包里导入模块,从模块里导入变量。在前面的例子中我们有一个ecommerce包,包含有两个模块,一个叫database.py模块,一个叫products.py模块。在database模块里有一个db变量,这个变量会在很多地方访问到。如果用import ecommerce.db取代import ecommerce.database.db会方便一些。

此时就要用到__init__.py,这个文件里可以包含任意变量或类的声明,而且作为包的一部分为我们使用。在这个例子中,如果ecommerce/__init__.py包含这样一句:

from .database import db

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

from ecommerce import db

导致ecommerce.py这个文件是一个模块而不是一个包,就是__init__.py在起作用,如果你想把所有代码都放在一个单独的模块里,过后又决定把它拆分为一个包里的多个包,__init__.py也可以帮助到你。其他模块想要访问这个新包,__init__.py仍然是一个切入点,但是在内部,代码仍然可以被组织成不同的模块或者子包。

总结

本章主要学习了:

  • 类的语法
  • 属性和方法
  • 初始化函数和构造函数
  • 模块和包
  • 相对导入和绝对导入
  • 访问控制以及它的局限性
原创粉丝点击