Python基础:对象基本概念、继承、列表元组对象、循环函数模块异常对象

2. 类

描述鸟类

class Brid(object):
    feather = True
    reproduction = "egg"

关键字class来定义一个类。类的名字就是鸟(Brid)。括号里有一个关键词object,也就是“东西“的意思,即某一个个体。我们把个体称为对象。一个类别下,可以有多个个体。

冒号和缩进说明了属于这个类的代码。在隶属于这个类别的程序块中,我们定义了两个量,一个用于说明鸟类有羽毛(feather),一个用于说明鸟类的繁殖方式(reproduction),这两个量称为类的属性(attribute)。

我们除了用数据性的属性来分辨类别外,有时也会根据这类东西能做什么事情来区分。比如,鸟会移动。这样,鸟和房屋的类别就区分开了。这些动作会带来一定的结果,比如移动导致位置的变化。这样的一些“行为”属性称为方法(method)。Python中,一般是通过在类的内部定义函数来说明方法。

class Bird(object):
    feather = True
    reproduction = "egg"
    def chirp(self, sound):
        print(sound)

我们给鸟类新增一个方法属性,就是表示鸟叫的方法chirp()。方法chirp看起来很像一个函数。它的第一个参数是self,是为了在方法内部引用对象自身。需要强调的是,无论该参数是否用到,方法的第一个参数必须是用于指代对象自身的self。

3. 对象

**通过调用类,我们可以创造出这个类下面的一个对象。**比如,我养了一只小鸡,叫summer。它是个对象,且属于鸟类。我们使用前面已经定义好的鸟类,产生这个对象:

summer = Bird()

通过这一句创建对象,并说明summer是属于鸟类的一个对象。现在,我们就可以使用鸟类中已经写好的代码了。作为对象的summer将拥有鸟类的属性和方法。对属性的引用是通过对象.属性(object.attribute)的形式实现的。比如说:

print(summer.reproduction)	# 打印'egg'

用上面的方式,我们得到summer所属类的繁殖方式。

此外,我们还可以调用方法,让summer执行鸟类允许的动作。比如:

summer.chirp("jijiji")	# 打印jijiji

在调用方法时,我们只传递了一个参数,也就是字符串"jijiji"。这正是方法与函数有所区别的地方。**尽管在定义类的方法时,我们必须加上self参数,但self只能在类定义的内部,所以在调用方法时不需要对self传入数据。**通过调用chirp()方法,我的summer就可以叫了。

对于一个类下的全部个体来说,某些属性可能存在个体差异。

因此,为了完整描述个体,除了共性的类属性外,我们还需要用于说明个性的对象属性。在类中,我们可以通过self来操作对象的属性。现在,我们拓展Bird类:

class Bird(object):
    def chirp(self, sound):
        print(sound)
    def set_color(self, color):
        self.color = color
        
summer = Bird()
summer.set_color("yellow")
print(summer.color)		# 打印yellow

在方法set_color()中,我们通过self参数设定了对象的属性color。和类属性一样,我们能通过对象.属性的方式来操作对象属性。由于对象属性依赖于self,所以我们必须在某个方法内部才能操作对象属性。因此,对象属性没办法像类属性一样,在类下方直接赋初值。

但Python还是提供了初始化对象属性的方法。Python定义了一系列特殊方法。特殊方法又被称为魔法方法(Magic Method)。特殊方法的方法名很特别,前后有两个下划线,比如__init__()__add__()__dict__()等。程序员可以在类定义中设定特殊方法。Python会以特定的方式来处理各个特殊方法。对于类的__init__()方法,Python会在每次创建对象时自动调用。因此,我们可以在__init__()方法内部来初始化对象属性:

class Bird(object):
    def __init__(self, sound):
        self.sound = sound
        print("my sound is: ", sound)
        
	def chirp(self):
        print(self.sound)
        
summer = Bird("ji")
summer.chirp()

在上面的类定义中,我们通过__init__()方法说明了这个类的初始化方式。每当对象建立时,比如创建summer对象时,__init__()方法就会被调用。它会设定sound这个对象属性。在后面的chirp()方法中,就可以通过self调用这一对象属性。除了设定对象属性外,我们还可以在__init__()中加入其它指令。这些指令会在创建对象时执行。在调用类时,类的后面可以跟一个参数列表。这里放入的数据将传给__init__()的参数。通过__init__()方法,我们可以在创建对象时就初始化对象属性。

除了操作对象属性外,self参数还有另外一个功能,就是能让我们在一个方法内部调用同一类的其他方法,比如:

class Bird(object):
    def chirp(self, sound):
        print(sound)
        
    def chirp_repeat(self, sound, n):
        for i in range(n):
            self.chirp(sound)
            
summer = Bird()
summer.chirp_repeat("ji", 10) # 重复打印'ji'十次

在方法chirp_repeat()中,我们通过self调用了类中的另一个方法chirp。

4.2 继承者们

1. 子类

类别本身还可以进一步细分成子类。在面向对象编程中,我们通过继承(Inheritance)来表达上述概念。

class Bird(object):
    feather = True
    reproduction = "egg"
    def chirp(self, sound):
        print(sound)
        
class Chicken(Bird):
    how_to_move = "walk"
    edible = True
    
class Swan(Bird):
    how_to_move = "swim"
    edible = False
    
summer = Chicken()
print(summer.feather)	# 打印True
summer.chirp("ji")	# 打印"ji"

新定义的鸡(Chicken)类,增加了两个属性:移动方式(how_to_move)和可以食用(edible)。

在类定义时,括号里面为Bird。这说明,鸡类是属于鸟类的一个子类,即Chicken继承自Bird。自然而然,鸟类就是鸡类的父类。Chicken将享有Bird的所有属性。尽管我们只声明了summer是鸡类,但它通过继承享有了父类的属性,比如数据性的属性feather,还有方法性的属性chirp()。新定义的天鹅(Swan)类,同样继承自鸟类。在创建一个天鹅对象时,该对象就自动拥有鸟类的属性。

继承提高了程序的可重复使用性。最基础的情况,是类定义括号中是object。类object其实是Python中的一个内置类,它充当了所有类的祖先。

2. 属性覆盖

如上所述,在继承的过程中,我们可以在子类中增加父类不存在的属性,从而增强子类的功能。此外,我们还可以在子类中替换父类已经存在了的属性,比如:

class Bird(object):
    def chirp(self):
        print("make sound")
        
class Chicken(Bird):
    def chirp(self):
        print("ji")
        
bird = Bird()
bird.chirp()

summer = Chicken()
summer.chirp()	# 打印'make sound'和'ji'

鸡类(Chicken)是鸟类(Bird)的子类。在鸡类(Chicken)中,我们定义了方法chirp()。这个方法在鸟类中也有定义。通过调用可以看出,鸡类会调用自身定义的chirp()方法,而不是父类中的chirp()方法。从效果上看,这就好像是父类中的方法chirp()被子类中的同名属性覆盖(override)了一样。

通过对方法的覆盖,我们可以彻底地改变子类的行为。但有的时候,子类的行为是父类行为的拓展。这时,我们可以通过super关键字在子类中调用父类中被覆盖的方法,比如:

class Bird(object):
    def chirp(self):
        print("make sound")
        
class Chicken(Bird):
    def chirp(self):
        super().chirp()
        print("ji")
        
bird = Bird()
bird.chirp()	# 打印"make sound"

summer = Chicken()
summer.chirp()	# 打印"make sound"和"ji"

在鸡类的chirp()方法中,我们使用了**super。它是一个内置类,能产生一个指代父类的对象。通过super,我们在子类的同名方法中调用了父类的方法。**这样,子类的方法技能执行父类中的相关操作,又能定义属于自己的额外操作。

调用super的语句可以出现在子类方法的第一句,也可以出现在子类方法的任意其它位置。

4.3 那些年,错过的对象

1. 列表对象

数据容器中的列表。它是一个类,用内置函数可以找到类的名字:

a = [1, 2, 5, 3, 5]
type(a)

根据返回的结果,我们知道a属于list类型,也就是列表类型。其实,所谓的类型就是对象所属类的名字。每个列表都属于list类。这个类时Python自带的,已经提前定义好的,所以称为内置类。当我们新建一个表时,实际上是在创建list类的一个对象。我们还可以用其他两个内置函数来进一步调查类的信息:dir()和help()。函数dir()用来查询一个类或者对象的所有属性。你可以尝试一下:

dir(list)

我们已经用help()函数查询了函数的说明文档。它还可以用于显示类的说明文档。你可以尝试一下:

help(list)

返回的不但有关于list类的描述,还简略的说明了它的各个属性。顺便提一下,制作类的说明文档的方式,与制作函数说明文档的方式类似,我们只需在类定义下用多行字符串加入自己想要的说明就可以了:

class HelpDemo(object):
    """
    This is a demo for using help() on a class
    """
    pass

print(help(HelpDemo))

程序中的pass是Python的一个特殊关键字,用于说明在该语法结构中“什么都不做”。这个关键字保留了程序结构的完整性。

通过上面的查询,我们看到类还有许多“隐藏技能”。比如下面一些list的方法,可以返回列表信息。

a = [1, 2, 3, 5, 9.0, "Good", -1, True, False, "Bye"]
a.count(5)	# 计数,看总共有多少个元素5
a.index(3)	# 查询元素3第一次出现时的下标

有些方法还允许我们对列表进行修改操作:

a.append(6)	# 在列表的最后添加一个新元素6
a.sort()	# 排序
a.reverse()	# 颠倒次序
a.pop()	# 去除最后一个元素,并将该元素返回
a.remove(2)	# 去除第一次出现的元素2
a.insert(0, 9)	# 在下标为0的位置插入9
a.clear()	# 清空列表

通过对方法的调用,列表的功能大为增加。

2. 元组与字符串对象

元组与列表一样,都是序列。但元组不能变更内容。因此,元组只能进行查询操作,不能进行修改操作:

a = (1, 3, 5)
a.count(5)	# 计数,看总共有多少个元素5
a.index(3)	# 查询元素3第一次出现的下标

字符串是特殊的元组,因此可以执行元组的方法:

a = "abc"
a.index('c')

尽管字符串是元组的一种,但字符串(string)有一些方法能改变字符串。这听起来似乎违背了元组的不可变性。其实,这些方法并不是修改字符串对象,而是删除原有字符串,在建立一个新的字符串。所以并没有违背元组的不可变性。

下面总结了字符串对象的方法。str为一个字符串,sub为str的一个子字符串。s为一个序列,它的元素都是字符串。width为一个整数,用于说明新生成字符串的宽度。这些方法常用于字符串的处理。

str = "Hello World!"
sub = "World"

str.count(sub)	# 返回:sub在str中出现的次数
str.find(sub)	# 返回: 从左开始,查找sub在str中第一次出现的位置。如果str中不包含sub,返回-1
str.index(sub)	# 返回,从左开始,查找sub在str中第一次出现的位置。如果str不包含sub,举出错误。
str.rfind(sub)	# 返回:从右开始,查找sub在str中第一次出现的位置。如果str中不包含sub,返回-1
str.rindex(sub)	# 返回,从右开始,查找sub在str中第一次出现的位置。如果str中不包含sub,举出错误
str.isalnum()	# 返回:True,如果所有字符串都是字母或数字
str.isalpha()	# 返回:True,如果所有的字符都是字母
str.isdigit()	# 返回:True,如果所有的字符都是数字
str.istitle()	# 返回:True,如果所有的词的首字母都是大小
str.isspace()	# 返回:True,如果所有的字符都是空格
str.islower()	# 返回:True,如果所有的字符都是小写字母
str.isupper()	# 返回:True,如果所有的字符都是大写字母
str.split([sep, [max]])	# 返回:从左开始,以空格我分隔符(separator),将str分割为多个子字符串,总共分割max次。将所得的子字符串放在一个表中返回。可以以str.split(",")的方式使用其他分隔符
str.rsplit([sep, [max]])	# 返回:从右开始,以空格为分隔符(separator),将str分割为多个子字符串,总共分割max次。将所得的子字符串放在一个表中返回。可以以str.rsplit(",")的方式使用其他分隔符。
str.join(s)	# 返回:将s中的元素,以str为分隔符,合并成一个字符串
str.strip([sub])	# 返回:去掉字符串开头和结尾的空格。
					# 也可以提供参数sub,去掉位于字符串开头和结尾的sub
str.replace(sub, new_sub)	# 返回:用一个新的字符串new_sub替换str中的sub
str.capitalize()	# 返回:将str的第一个字母大写
str.lower()		# 返回:将str全部字母改为小写
str.upper()		# 返回:将str的全部字母改为大写
str.swapcase()	# 返回:将str大写字母改为小写,小写字母改为大写
str.title()		# 返回:将str的每个词(以空格分隔)的首字母大写
str.center(width)	# 返回:长度为width的字符串,将原字符串放入该字符串的中心,其他空余位置为空格。
str.ljust(width)	# 返回:长度我width的字符串,将原字符串左对齐放入该字符串,其他位置为空格。
str.rjust(width)	# 返回:长度为width的字符串,将原字符串右对齐放入该字符串,其他空余位置为空格。

3. 词典对象

词典同样是一个类:

example_dict = {"a":1, "b":2}
type(example_dict)

我们可以通过词典的keys()方法,来循环遍历每个元素的键

for k in example_dict.keys:
    print(k)
for k in example_dict.keys():
    print(example_dict[k])

通过values()方法,可以遍历每个元素的值。或者用items方法,直接遍历每个元素(items()方法得到的是一个元组,这里有两个变量参与循环,实际上相当于解打包元组了):

for v in example_dict.values():
    print(v)
   
for k,v in example_dict.items():
    print(k, v)

我们可以用clear()方法,清空整个词典:

example_dict.clear() # 清空exampe_dict,example_dict变为{ }

4.4 意想不到的对象

1. 循环对象

Python中许多语法结构都是由对象实现的,循环就可以通过对象实现。在Python3时代,循环对象正在成为循环的标准形式。

那么,什么是循环对象呢?所谓的循环对象包含有一个__next__()方法。这个方法的目的是生成循环的下一个结果。在生成循环的所有结果之后,该方法将抛出StopIteration异常。

当一个像for这样的循环语法调用循环对象时,它会在每次循环的时候调用__next__()方法。我们多次调用__next__()方法,将不断返回列表的值,直到出现异常:

example_iter = iter([1, 2])
example_iter.__next__()		# 显示1
example_iter.__next__()		# 显示2
example_iter.__next__()		# 出现StopIteration异常。

我们上面重复调用__next__()的过程,就相当于手动进行了循环。我们可以把循环对象包裹在for中自动进行循环:

for item in iter([1, 2]):
    print(item)

在这里,for结构自动调用__next__()方法,将该方法的返回值赋予给item。循环知道出现StopIteration的时候结束。当然,我们可以省去内置函数iter的转换。这是因为,for结构会自动执行这一转换。

相对于序列,循环对象的好处在于:不用在循环还没开始的时候,就生成要使用的元素。所有要使用的元素可以在循环过程中逐渐生成。这样,不仅节省了空间,提高了效率,还会使编程更加灵活。

我们可以借助生成器(generator)来自定义循环对象。生成器的编写方法和函数定义类似,只是在return的地方改为yield。生成器中可以有多个yield。当生成器遇到一个yied时,会暂停运行生成器,返回yield后面的值。当再次调用生成器的时候,会从刚才暂停的地方继续运行,直到下一个yield。生成器自身又构成一个循环对象,每次循环使用一个yield返回的值。

下面是一个生成器:

def gen():
    a = 100
    yield a
    a = a*8
    yield a
    yield 1000

该生成器共有三个yield,如果用作循环对象时,会进行三次循环。

for i in gen():
    print(i)

再考虑下面一个生成器:

def gen():
    i = 0
    while i < 10000000:
        i = i + 1
        yield i

这个生成器能产生10000000个元素。如果先创建序列保存这10000000个元素,再遍历循环,那么这个序列将占用大量的空间。出于同样的原因,Python中的内置函数range()返回的是一个循环对象,而不是一个序列

2. 函数对象

在Python中,函数也是一种对象。实际上,任何一个有__call__()特殊方法的对象都被当做是函数。比如下面的例子:

class SampleMore(object):
    def __call__(self, a):
        return a + 5

add_five = SampleMore()	# 生成函数对象
print(add_five(2))	# 像一个函数一样调用函数对象,结果为7。

add_five为SampleMore类的一个对象,当被调用时,add_five执行加5操作。

3. 模块对象

Python中的模块对应一个.py文件。模块也是对象。比如:我们直接引入标准库中的模块time:

import time
print(dir(time))

可以看到,time有很多属性可以调用,例如sleep()方法。我们之前用import语句引入其他文件中定义的函数,实际上是引入模块对象的属性,比如:

from time import sleep
sleep(10)
print("wake up")

模块time的sleep()会中止程序。调用时的参数说明给了中止的时间。

一次性引入模块的所有属性:

from time import *
sleep(10)

既然知道了sleep()是time的一个方法,那么我们当然可以利用对象.属性的方式来调用它。

import time
time.sleep(10)

我们在调用方法时附带上了对象名。这样做的好处是可以拓展程序的命名空间,避免同名冲突。例如,如果两个模块中都有sleep()方法,那么我们可以通过不一样的模块名开区分开来。在my_time.py中写入函数:

def sleep(self):
    print("I am sleeping.")

在main.py中引入内置模块time和自定义模块my_time:

import time
import my_time

time.sleep()
my_time.sleep()

上面的两次对sleep()方法的调用中,我们通过对象名区分出了不同的sleep()。

在引入模块时,我们还可以给模块换个名字:

import time as t
t.sleep(10)

在引入名字较长的模块时,这个换名字的办法能有效地挽救程序员的手指。

可以将功能相似的模块放在同一个文件夹中,构成一个模块包。比如放在this_dir中:

import this_dir.module

引入this_dir文件夹中的module模块。

该文件夹中必须包含一个__init__.py的文件,提醒Python,该文件夹为一个模块包。__init__.py可以是一个空文件。

每个模块对象都有一个__name__属性,用来记录模块的名字,例如:

import time
print(time.__name__)

当一个.py文件作为主程序运行时,比如python foo.py,这个文件也会有一个对应的模块对象。但这个模块对象的__name__属性会是__main__。因此,我们在很多.py文件中可以看到下面的语句:

if __name__ == "__main__":

它的意思是说,如果这个文件作为一个主程序运行,那么将执行下面的操作。有的时候,一个.py文件中同时有类和对象的定义,以及对它们的调用。当这些.py文件作为库引入时,我们可能并不希望执行这些调用。通过吧调用语句放到上面的if中,就可以在调用时不执行这些调用语句了。

4. 异常对象

前面我们提到过,可以在程序中加入异常处理的try结构,捕捉程序中出现的异常。实际上,我们捕捉到的也是一个对象,比如:

try:
    m = 1/0
except ZeroDivisionError as e:
    print("Catch NameError in the sub-function")
    
print(type(e))    # 类型为“exceptions.ZeroDivisionError”
print(dir(e))	# 异常对象的属性
print(e.message)	# 异常信息integer division or modulo by zero

利用except…as…的语法,我们在except结果中用e代表捕获到的类型对象。关键字except直接跟随的ZeroDivisionError实际上是异常对象的类。正因为如此,我们在举出异常时会创建一个异常对象:

raise ZeroDivisionError()

在Python中,循环、函数、模块、异常都是某种对象。

附录A 代码规范

类的命名采用首字母大写的英文单词。如果由多个单词连接而成,则每个单词的首字母都大写。单词之间不出现下划线。

对象名、属性名和方法名,全部用小写字母。单词之间用下划线连接。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值