面向对象高级编程

使用slots

正常情况下,当我们定义了一个Class,并创建了一个关于Class的instance后,我们可以该instance绑定任何属性和方法,这表现了动态语言的灵活性。

class Student(object):
    pass

给instance绑定属性:

>>>s = Student()
>>>s.name = 'Michael'
>>>print('s.name')
Michael

给instance绑定一个方法:

>>> def set_age(self, age): # 定义一个函数作为实例方法
...     self.age = age
...
>>> from types import MethodType
>>> s.set_age = MethodType(set_age, s) # 给实例绑定一个方法
>>> s.set_age(25) # 调用实例方法
>>> s.age # 测试结果
25

注意,给一个instance绑定的方法对另一个instance不成立

>>>s2 = Student
>>>s2.set_age(25)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'set_age'

为了解决这个,给所有的instance都绑定方法,我们选择给Class绑定方法

>>> def set_score(self, score):
...     self.score = score
...
>>> Student.set_score = set_score

给Class绑定方法后,所有的instance都可以调用。这体现了动态语言的优点。

使用slots(限制instance的属性)

为了达到限制的目的,可以再定义Class的时候,定义一个特殊的slots变量,来限制该Class实例可以添加的属性。

class Student(object):
    __slots__ = ('name','age') #用tuple定义允许绑定的属性名

验证:

>>> s = Student() # 创建新的实例
>>> s.name = 'Michael' # 绑定属性'name'
>>> s.age = 25 # 绑定属性'age'
>>> s.score = 99 # 绑定属性'score'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'

注意,slots定义的属性仅仅对当前类起作用,对集成的子类不起作用。除非在子类中也定义slots,这样,子类实例允许定义的属性就是自身的slots加上父类的slots

使用@property

为了解决在绑定属性时,虽然将属性直接暴露出去但是无法检查参数的问题(导致例子中的成绩可以随意修改)

s = Student()
s.score = 9999

为了解决这样的问题可以添加方法来get和set

class Student(object):

    def get_score(self):
         return self._score

    def set_score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

这样就可以:

>>> s = Student()
>>> s.set_score(60) # ok!
>>> s.get_score()
60
>>> s.set_score(9999)
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

为了简化,使类的属性即可以被检查又可以通过访问属性的简单方式进行访问,可以使用Python内置的装饰器:

class Student(object):
    """docstring for Student"""
    @property
    def  score(self):
        return self._score

    @score.setter
    def score(self,value):
        if not isinstance(value,int):
            raise ValueError('score mmust be an integer')
        if value < 0 or value > 0:
            raise ValueError('score must between 0-100')
        self._score = value

实质就是将一个方法变成属性,方便访问。
把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作:

>>> s = Student()
>>> s.score = 60 # OK,实际转化为s.set_score(60)
>>> s.score # OK,实际转化为s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

定义只读属性的方法:只定义getter方法,不定义setter方法

多重继承

继承是面向对象编程的三个重要特点之一,因为通过继承,子类就可以扩展父类的功能。

#    Dog - 狗狗;
#    Bat - 蝙蝠;
#    Parrot - 鹦鹉;
#    Ostrich - 鸵鸟。
#    哺乳类:能跑的哺乳类,能飞的哺乳类;
#    鸟类:能跑的鸟类,能飞的鸟类。
# 使用多重继承
class Animal(object):
   pass

# 大类:
class Mammal(Animal):
   pass

class Bird(Animal):
   pass

# 各种动物:
class Dog(Mammal):
   pass

class Bat(Mammal):
   pass

class Parrot(Bird):
   pass

class Ostrich(Bird):
   pass

为了给动物加上Runnable和Flyable的功能,可以定义相应的类:

class Runnable(object):
    def run(self):
        print('Running...')

class Flyable(object):
    def fly(self):
        print('Flying...')

对于需要Runnable功能的动物,就多继承一个Runnable,例如Dog:

class Dog(Mammal,Runnable):
    pass

对于需要Flyable功能的动物,就多继承一个Flyable,例如Bat:

class Bat(Mammal, Flyable):
    pass

Mixln

在设计类的继承关系时,通常主线都是单一继承下来的,但是如果需要“混入”特殊的功能,通过多重继承就可以实现(Mixln),这是因为通过多重继承子类就可以同时获得多个父类的功能。
为了更好地看出继承关系,我们把Runnable和Flyable改为RunnableMixIn和FlyableMixIn,类似的,你还可以定义出肉食动物CarnivorousMixIn和植食动物HerbivoresMixIn,让某个动物同时拥有好几个MixIn:

class Dog(Mannal,RunnableMixln,CarnivorousMixln):
   pass

MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。

定制类

除了之前介绍过的slotslen方法之外还有很多有特殊用途的函数可以帮我们定制类。

str

>>> class Student(object):
...     def __init__(self, name):
...         self.name = name
...
>>> print(Student('Michael'))
<__main__.Student object at 0x109afb190>

#为了改变上面不美观的输出结果,需要定义好__str__()方法,返回一个好看的字符串

>>> class Student(object):
...     def __init__(self, name):
...         self.name = name
...     def __str__(self):
...         return 'Student object (name: %s)' % self.name
...
>>> print(Student('Michael'))
Student object (name: Michael)

但是直接敲变量不用print打印出来的实例还是不好看:

>>> class Student(object):
...     def __init__(self, name):
...         self.name = name
...     def __str__(self):
...         return 'Student object (name: %s)' % self.name
...
>>> print(Student('Michael'))
Student object (name: Michael)

这是因为直接显示变量调用的不是str(),而是repr(),两者的区别是str()返回用户看到的字符串,而repr()返回程序开发者看到的字符串,也就是说,repr()是为调试服务的。
解决方案就是直接将事先定义好的str赋值给repr

class Student(object):
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return 'Student object (name=%s)' % self.name
    __repr__ = __str__

iter

如果一个类想被用于for... in循环,就必须实现一个iter()方法,该方法返回一个迭代的对象,然后python的for循环就会不断的调用该迭代的next()方法得到循环的下一个值,知道StopIteration.
例如:

class Fib(object):
    def __init__(self):
        self.a, selff.b = 0,1  #初始化两个计数器a,b 
    def __iter__(self):      
        return self #实例本身就是迭代对象,所以返回自己
    def __next__(self):
        self.a,self.b = self.b,self.a+self.b    #计算下一个值
        if self.a > 100000: # 退出循环的条件
            raise StopIteration()
        return self.a # 返回下一个值 

getitem

上面的例子中的instance虽然可以yongfor循环,但是不可以完全把它当做list处理:

>>> Fib()[5]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'Fib' object does not support indexing

为了取出下标元素,需要实现getitem方法:

class Fib(object):
    def __getitem__(self, n):
        a, b = 1, 1
        for x in range(n):
            a, b = b, a + b
        return a

为了进一步的实现list中的slice操作:

#我们必须让__getitem__()判断传入的参数是int还是slice
class Fib(object):
    """docstring for Fib"""

    def __getitem__(self, n):
        if isinstance(n, int):
            a, b = 1, 1
            for x in range(n):
                a, b = b, a + b
            return a
        if isinstance(n, slice):
            start = n.start
            stop = n.stop
            if start is None:
                start = 0
            a, b = 1, 1
            L = []
            for x in range(stop)
                if x >= start:
                    L.append(a)
                a, b = b, a + b
            return L

如果要让getitem实现对step参数和负数的处理还需要进一步的完善。

getattr

正常情况下,当调用类的方法或属性时,如果不存在就会报错。
例子:

class Student(object):

    def __init__(self):
        self.name = 'Michael'

#调用name属性,没有问题,但是调用不存在的score属性就有问题了
>>> s = Student()
>>> print(s.name)
Michael
>>> print(s.score)
Traceback (most recent call last):
  ...
AttributeError: 'Student' object has no attribute 'score'

要避免这个错误,除了可以加上一个score属性外,Python还有另一个机制,那就是写一个getattr()方法,动态返回一个属性。修改如下:

class Student(object):

    def __init__(self):
        self.name = 'Michael'

    def __getattr__(self, attr):
        if attr=='score':
            return 99

当调用不存在的属性时,比如score,Python解释器会试图调用getattr(self, 'score')来尝试获得属性,这样,我们就有机会返回score的值:

>>> s = Student()
>>> s.name
'Michael'
>>> s.score
99

注意,只有在没有找到属性和的情况下才调用_getattr,已有的属性不会在__getattr中查找。
注意到任意调用如s.abc都会返回None,这是因为我们定义的getattr默认返回就是None。要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError的错误:

class Student(object):

    def __getattr__(self, attr):
        if attr=='age':
            return lambda: 25
        raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)

call

一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()来调用,也可以通过call直接在instance上调用(不用显式的写出method)。

class Student(object):
    def __init__(self, name):
        self.name = name

    def __call__(self):
        print('My name is %s.' % self.name)

#调用方式
>>>s  = Student('Michael')
>>>s()
My name is Michael

除此之外,call()还可以定义参数,这让对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者本身就没什么根本的区别。
更多的时候我们判断一个对象是否可以被调用,能被调用的对象是一个Callable对象:

>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None)
False
>>> callable('str')
False

使用枚举类

当我们定义常量的时候,可以使用大写变量通过整数来定义:

JAN = 1
FEB = 2
MAR = 3
...
NOV = 11
DEC = 12

但是变量的类型仍然是int。
更好的方法是为这样的枚举类型定义一个Class类型,每个变量都是Class的一个唯一的实例

#获得Month类型的枚举类
from enum import Enum
Month = Enum ('Month',('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')) 

#可以直接使用Month.Jan来引用一个常量,或者枚举它的全部成员:
for name,member in Month.__members__.items():
    print(name,'=>',member,',',member.value)

value属性则是自动赋给成员的int常量,默认从1开始计数。
如果需要更精确地控制枚举类型,可以从Enum派生出自定义类:

from enum import Enum, unique

@unique   #@unique装饰器可以帮助我们检查保证没有重复值。
class Weekday(Enum):
    Sun = 0 # Sun的value被设定为0
    Mon = 1
    Tue = 2
    Wed = 3
    Thu = 4
    Fri = 5
    Sat = 6

使用元类

type()

动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。
前情提要:已经写好了hello.py的Module:

class Hello(object):
    def  hello(self,name='world'):
        print('Hello,%s.' %name)

当Python解释器载入hello Module使,就会依次执行该Module的所有语句,执行结果就是动态的创建一个Hello的class对象(类也是一种对象)。

>>> from hello import Hello
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class 'hello.Hello'>

type()函数可以查看一个类型或变量的类型,Hello是一个class,它的类型就是type,而h是一个实例,它的类型就是class Hello。
type()函数可以返回一个对象的类型,又可以创建出新的类型。

#可以通过type()函数创建出Hello类,而无需通过之前的方法
>>> def fn(self, name='world'): # 先定义函数
...     print('Hello, %s.' % name)
...
>>> Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class '__main__.Hello'>

要创建一个Class对象,type()函数依次传入3个参数:
1.class的名称
2.继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法
3.class的方法名称与函数绑定
通过type()函数创建的类和直接写Class是一样的,因为Python解释器遇到Class定义时,仅仅是扫面一下class定义的语法,然后调用type()函数创建出class
正常情况下,我们都用class Xxx...来定义类,但是,type()函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。

metaclass

除了使用type()动态创建类以外,要控制类的创建行为,还可以使用metaclass:先定义metaclass,就可以创建类,最后创建instance(可以将类看成是metaclass创建的实例)
例子:使用metaclass给自定义的Mylist增加一个add方法

#metaclass是类的模板,必须从type类派生
class ListMetaclass(type):
    def __new__(cls,name,bases,attrs):
        attrs['add'] = lambda self,value : self.append(value)
        return type.__new__(cls,name,bases,attrs)

有了ListMetaclass,我们在定义类的时候还要指示使用ListMetaclass来定制类,传入关键字metaclass:

class Mylist(list,metaclass=ListMetaclass):
    pass

当我们传入关键字参数metaclass时,魔术就生效了,它指示Python解释器在创建MyList时,要通过ListMetaclass.new()来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后,返回修改后的定义。

new()方法接收到的参数依次是:
当前准备创建的类的对象;
类的名字;
类继承的父类集合;
类的方法集合。

#测试:

>>> L = MyList()
>>> L.add(1)
>> L
[1]

#普通的list没有add()方法
>>> L2 = list()
>>> L2.add(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute 'add'

元类的方法可以用在写ApI或者数据库中的ORM(object relationship mapping)等方面。

为了加深理解,附上一个知乎上的回答:
[type和object的关系]https://www.zhihu.com/question/38791962/answer/78172929

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容