Python基础20-面向对象:静态、组合、继承与派生、多态、封装、反射、内置attr方法等

无人久伴 提交于 2020-01-28 17:01:22

目录

静态

静态属性@property

类方法@classmethod

静态方法@staticmethod

组合

继承与派生

继承与派生

继承顺序

在子类中调用父类方法与super

多态

封装

反射

模块的动态导入

内置的attr方法

带双下户线的attr方法__setattr__、__getattr__、__delattr__

__getattr__方法的牛逼用法

继承方式完成包装


静态

静态属性@property

以类Room为例,Room类里面存储了width、length、height属性,面积、体积都是通过这些属性计算出来的,没有必要再存一份面积、体积。使用装饰器@property可以把有返回值的方法变成属性一样,这样减少了冗余,对调用者来说就像直接访问属性一样方便。这就是静态属性。

class Room:
    def __init__(self, name, owner, width, length, height):
        self.name = name
        self.owner = owner
        self.width = width
        self.length = length
        self.height = height
        pass

    @property
    def area(self):
        """
        计算面积
        :return: 面积
        """
        return self.width * self.length
        pass

    @property
    def volume(self):
        """
        计算体积
        :return: 体积
        """
        return self.width * self.length * self.height
        pass

    pass


"""
静态属性

类里面存储了width、length、height属性
面积、体积都是通过这些属性计算出来的,没有必要再存一份面积、体积
使用装饰器@property可以把有返回值的方法变成属性一样,这样减少了冗余
对调用者来说就像直接访问属性一样方便
"""
r1 = Room('五路居', 'Kevin', 18, 15, 3)
r2 = Room('回龙观', 'Alice', 15, 20, 3)
print(r1.area)
print(r2.area)
print(r1.volume)
print(r2.volume)
# Kevin 住的 五路居 总面积是 300平米 体积是 810立方米
# Alice 住的 回龙观 总面积是 300平米 体积是 900立方米

类方法@classmethod

我们有时候访问类属性,不需要访问对象,而所有带self参数的方法都要求先有对象实例,这就产生了矛盾。那么,就得用classmethod装饰器,被classmethod装饰的方法都自带cls参数,直接使用类名调用类方法,访问类属性。

class Room:
    tag = 1

    def __init__(self, name, owner, width, length, height):
        self.name = name
        self.owner = owner
        self.width = width
        self.length = length
        self.height = height
        pass

    @property
    def area(self):
        """
        计算面积
        :return: 面积
        """
        return self.width * self.length
        pass

    @property
    def volume(self):
        """
        计算体积
        :return: 体积
        """
        return self.width * self.length * self.height
        pass

    @classmethod
    def tell_info(cls, x):
        print('类方法访问类属性tag--->', cls.tag, x)
        pass

    pass

"""
类方法

我们有时候访问类属性,不需要访问对象,而所有带self参数的方法都要求先有对象实例,这就产生了矛盾。
那么,就得用classmethod装饰器,被classmethod装饰的方法都自带cls参数,直接使用类名调用类方法,访问类属性。
"""
Room.tell_info('入参x')
# 类方法访问类属性tag---> 1 入参x

静态方法@staticmethod

静态方法本身,既和类无关,也个对象实例无关。类可以调用静态方法,对象实例也可以调用静态方法。按照规范应该由类来直接调用。静态方法是类的工具包。比如Java里面的Math类就定义了log,exp等静态方法。这些方法和数字类、数字对象都无关,但是可以做运算。

class Room:
    tag = 1

    def __init__(self, name, owner, width, length, height):
        self.name = name
        self.owner = owner
        self.width = width
        self.length = length
        self.height = height
        pass

    @property
    def area(self):
        """
        计算面积
        :return: 面积
        """
        return self.width * self.length
        pass

    @property
    def volume(self):
        """
        计算体积
        :return: 体积
        """
        return self.width * self.length * self.height
        pass

    @classmethod
    def tell_info(cls, x):
        print('类方法访问类属性tag--->', cls.tag, x)
        pass

    @staticmethod
    def bathroom(p):
        print('%s 在房间里洗澡' % p)
        pass

    pass


"""
静态方法

静态方法本身,既和类无关,也个对象实例无关。
类可以调用静态方法,对象实例也可以调用静态方法。按照规范应该由类来直接调用。
静态方法是类的工具包。
比如Java里面的Math类就定义了log,exp等静态方法。
这些方法和数字类、数字对象都无关,但是可以做运算。
"""
Room.bathroom('Kevin')
# Kevin 在房间里洗澡

组合

类的对象本身也可以作为另一个类的属性。比如点这个类的对象可以作为圆的圆心。

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

    pass


class Circle:
    def __init__(self, center, radius):
        self.center = center  # 圆心
        self.radius = radius  # 半径
        pass

    pass


p1 = Point(2, 2)
c1 = Circle(p1, 3)
print(c1.__dict__)
# {'center': <__main__.Point object at 0x00000000026C9BA8>, 'radius': 3}

# 圆的位置的x坐标
print(c1.center.x)

继承与派生

继承与派生

当一个类是另一个类的时候,就存在着继承关系,我们把是另一个类的共同的属性提取出来,做成父类,一个类从这个父类继承。比如,猫、狗是动物,他们都有毛色、品种,都会吃、叫,那么父类就是动物,猫、狗从动物继承。继承操作应尽量定义一个接口父类,子类继承接口父类,并在子类中实现继承下来的接口。这就类似于Java中的abstract class或interface。

怎么实现接口父类呢?那么就是abc模块(Abstract Base Class)。接口父类里面加上metaclass=abc.ABCmeta,这样定义的父类才能使子类必须实现父类接口,否则子类在实例化时会报错。

当一个类从父类继承以后,还可以自己新增更多只属于自己的属性,这就是派生。猫、狗都从动物继承,猫还可以派生九条命数据属性和上树方法,狗还可以派生出狗刨方法。

import abc


class Animal(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def bark(self):
        pass

    @abc.abstractmethod
    def eat_food(self, food):
        pass

    pass


class Cat(Animal):
    def bark(self):
        print('喵喵!')
        pass

    def eat_food(self, food):
        print('猫吃%s' % food)
        pass

    pass


class Dog(Animal):
    def bark(self):
        print('汪汪!')
        pass

    def eat_food(self, food):
        print('狗吃%s' % food)
        pass

    pass


c1 = Cat()
c1.bark()
c1.eat_food('鱼')
# 喵喵!
# 猫吃鱼

d1 = Dog()
d1.bark()
d1.eat_food('骨头')
# 汪汪!
# 狗吃骨头

继承顺序

Java只支持单继承,一个类只有一个父类,但是可以实现多个interface。Python不同,Python支持多继承,可以有多个父类,那么就涉及到一个继承顺序的问题。假设这样的场景,F→D→B→A,F→E→C→A。也就是F继承D、E,D继承B继承A,E继承C继承A。继承顺序来自于类的MRO列表(Method Resolve Order 方法解析顺序列表),我们可以通过__mro__属性来看到这个顺序。

Python调用方法的时候,先从子类自己找,如果找不到再按照类的MRO列表顺序找,如果对于继承的方法有两个符合要求的实现,那么调用第一个父类中的实现。

class A:
    def test(self):
        print('A')
        pass


class B(A):
    def test(self):
        print('B')
        pass


class C(A):
    def test(self):
        print('C')
        pass


class D(B):
    def test(self):
        print('D')
        pass


class E(C):
    def test(self):
        print('E')
        pass


class F(E, D):
    def test(self):
        print('F')
        pass


f1 = F()
f1.test()  # F
print(F.__mro__)
# (<class '__main__.F'>, 先从自己找
# <class '__main__.E'>, 然后从第一个父类找
# <class '__main__.C'>, 
# <class '__main__.D'>, 
# <class '__main__.B'>, 
# <class '__main__.A'>, 直至找到根基类
# <class 'object'>)以及object

同理,对于数据属性也是一样的顺序。

class A:
    def __init__(self, x=10):
        self.x = x
        pass

    pass


class B:
    def __init__(self, x=17):
        self.x = x
        pass

    pass


class C(B, A):
    def __init__(self):
        super().__init__()
        pass

    def show_info(self):
        print(self.x)
        pass

    pass


c = C()
c.show_info()  # 17

在子类中调用父类方法与super

关于这一部分,我们先看看文档里面怎么写的。我们可以直接调用super()来得到父类,然后调用父类方法。或者我们可以用super(__class__,<first argument>),也就是super(子类名,第一个参数self)。

    def __init__(self, type1=None, type2=None): # known special case of super.__init__
        """
        super() -> same as super(__class__, <first argument>)
        super(type) -> unbound super object
        super(type, obj) -> bound super object; requires isinstance(obj, type)
        super(type, type2) -> bound super object; requires issubclass(type2, type)
        Typical use to call a cooperative superclass method:
        class C(B):
            def meth(self, arg):
                super().meth(arg)
        This works for class methods too:
        class C(B):
            @classmethod
            def cmeth(cls, arg):
                super().cmeth(arg)
        
        # (copied from class doc)
        """
        pass

举个栗子。

class Vehicle:
    def __init__(self, name, speed, load):
        self.name = name
        self.speed = speed
        self.load = load
        pass

    def run(self):
        print('启动了!')
        pass

    def show_info(self):
        print(self.name, self.speed, self.load)
        pass

    pass


class Subway(Vehicle):
    def __init__(self, name, speed, load, line):
        super(Subway, self).__init__(name, speed, load)
        self.line = line
        pass

    def run(self):
        super().run()
        print('%s %s 线,以 %s 速度前进。' % (self.name, self.line, str(self.speed)))
        pass

    def show_info(self):
        print(self.name, self.speed, self.load, self.line)
        pass

    pass


line13 = Subway('北京地铁', '60km/h', 1200, '13')
# 北京地铁 60km/h 1200 13
line13.show_info()
# 启动了!
line13.run()
# 北京地铁 13 线,以 60km/h 速度前进。

多态

多态是一种运行时动态绑定的概念。多态与集成是伴随出现的,两者不能割裂开来。

回忆一下Java,方法形参为abstract class或者interface,调用时传入的实参是实现了抽象类的子类对象或者实现了接口的类的对象。这样在调用的时候,动态调用的是实现以后的方法。

在Python中也有多态的概念,也是运行时动态绑定的概念。回忆一下len方法,不管传入的是str、dict、list、tuple、set还是其他什么类型,都可以获取到长度。实际上,这些对象都实现了__len__方法,在调用len函数的时候,都动态执行了各自对象实现的__len__方法。不同的对象,调用相同的方法,每个对象方法的实现不一样实现不同的功能,这就是多态。

我们回到猫、狗的例子。看一下多态polymorphism是怎么实现的。

import abc


class Animal(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def bark(self):
        pass

    @abc.abstractmethod
    def eat_food(self, food):
        pass

    pass


class Cat(Animal):
    def bark(self):
        print('喵喵!')
        pass

    def eat_food(self, food):
        print('猫吃%s' % food)
        pass

    pass


class Dog(Animal):
    def bark(self):
        print('汪汪!')
        pass

    def eat_food(self, food):
        print('狗吃%s' % food)
        pass

    pass


def feed_pet(obj, food):
    """
    喂宠物吃东西
    :param obj:
    :param food:
    :return:
    """
    obj.eat_food(food)
    pass


def play_with_pet(obj):
    """
    和宠物玩,宠物会叫
    :param obj:
    :return:
    """
    obj.bark()
    pass


if __name__ == '__main__':
    c1 = Cat()
    d1 = Dog()

    feed_pet(c1, '鱼')  # 猫吃鱼
    feed_pet(d1, '骨头')  # 狗吃骨头
    play_with_pet(c1)  # 喵喵!
    play_with_pet(d1)  # 汪汪!
    pass

 

封装

封装就是隐藏内部的实现逻辑以及不允许用户直接访问的数据。这样有利于内部逻辑的实现,也防止了用户意外修改本不应该修改的数据。

回忆一下Java是怎么实现封装的?是通过关键字private、[不加关键字default]、protected和public四种访问权限关键字来实现封装的。每种修饰都对应着私有、包、子类、公有的不同访问权限。

Java的封装实现
 

私有权限

只有类自己可以访问到

包权限

同一个包下的其他类可以访问

子类权限

继承这个类的子类可以访问

子类和父类可能不在一个包

公有权限

任何地方都可以访问

private
【不加关键字default】
protected
public

那么Python怎么实现封装的呢?比较绕。我们注意一下Python约定的规则,一个、两个下划线开头表示外部不应直接访问的属性。

# polymorphism.py
import abc


class Animal(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def bark(self):
        pass

    @abc.abstractmethod
    def eat_food(self, food):
        pass

    pass


class Cat(Animal):
    fur_color = 'blue'
    _fur_color = '_blue'
    __fur_color = '__blue'

    def bark(self):
        print('喵喵!')
        pass

    def eat_food(self, food):
        print('猫吃%s' % food)
        pass

    pass


class Dog(Animal):
    def bark(self):
        print('汪汪!')
        pass

    def eat_food(self, food):
        print('狗吃%s' % food)
        pass

    pass


def feed_pet(obj, food):
    """
    喂宠物吃东西
    :param obj:
    :param food:
    :return:
    """
    obj.eat_food(food)
    pass


def play_with_pet(obj):
    """
    和宠物玩,宠物会叫
    :param obj:
    :return:
    """
    obj.bark()
    pass


if __name__ == '__main__':
    c1 = Cat()
    d1 = Dog()

    feed_pet(c1, '鱼')  # 猫吃鱼
    feed_pet(d1, '骨头')  # 狗吃骨头
    play_with_pet(c1)  # 喵喵!
    play_with_pet(d1)  # 汪汪!
    pass

调用的代码,这里要讲述的是Python里面一个、两个下划线定义的属性只是约定,但并不是真正意义上的封装。一个下划线开头的属性可以直接被访问到。两个下划线开头的属性被重新命名了,命名为_类名__属性,比如__fur_color就被转换为_Cat__fur_color,这个属性可以被直接访问到。

结论是,Python中并不像Java那样严格定义了封装,因为封装不只是把属性隐藏,被隐藏的那部分怎么被外部访问到也是封装要考虑的内容。因此Python中并没有严格定义了访问权限的封装。

# packaging.py
from polymorphism import *

c1 = Cat()
d1 = Dog()
play_with_pet(c1)
play_with_pet(d1)

# 注意Python不会真的封装
# 一个、两个下划线只是一种约定
# 注意Cat的__dict__里面两个下划线的属性__fur_color被重命名成_Cat__fur_color
print(c1.fur_color)  # blue
print(c1._fur_color)  # _blue
print(c1._Cat__fur_color)  # __blue
print(Cat.__dict__)
# {'__module__': 'polymorphism',
# 'fur_color': 'blue',
# '_fur_color': '_blue',
# '_Cat__fur_color': '__blue',
# 'bark': <function Cat.bark at 0x0000000003BAA8C8>,
# 'eat_food': <function Cat.eat_food at 0x0000000003BAA950>,
# '__doc__': None,
# '__abstractmethods__': frozenset(),
# '_abc_registry': <_weakrefset.WeakSet object at 0x0000000003B8D940>,
# '_abc_cache': <_weakrefset.WeakSet object at 0x0000000003B8D978>,
# '_abc_negative_cache': <_weakrefset.WeakSet object at 0x0000000003B8D9B0>,
# '_abc_negative_cache_version': 47}

反射

先介绍一下hasattr、getattr、setattr、delattr四个内置函数,分别用于查、取、增改、删除对象的属性。这四个方法都利用了反射机制,在运行时要对象实例自己清楚自己有什么、可以干什么。

先来看hasattr和getattr。

class FtpClient:
    def __init__(self, addr):
        print('ftp连接中……')
        self.addr = addr
        pass

    # def put(self):
    #     print('文件上传中……')
    #     pass

    pass


if __name__ == '__main__':
    fc = FtpClient('1.1.1.1')
    if hasattr(fc, 'put'):
        # hasattr传入对象和字符串,看是否有字符串名字的属性,返回布尔值
        # getattr传入对象和字符串,获取属性
        func_put = getattr(fc, 'put')
        func_put()
    else:
        print('put功能尚未完成,请继续其他工作!')
        pass
# ftp连接中……
# put功能尚未完成,请继续其他工作!

#========== 或者用另一种实现,利用getattr的default参数 ==========

if __name__ == '__main__':
    fc = FtpClient('1.1.1.1')
    func_put = getattr(fc, 'put', lambda x=None: print('put功能尚未完成,请继续其他工作!'))
    func_put()
    pass

至于setattr和delattr。接上面FtpClient的代码可以动态增加减少属性。

if __name__ == '__main__':
    def func_get():
        print('get from ftp')
        pass
    setattr(FtpClient, 'get', func_get)
    print(FtpClient.__dict__)
    # {'__module__': '__main__', 
    # '__init__': <function FtpClient.__init__ at 0x00000000028B50D0>, 
    # '__dict__': <attribute '__dict__' of 'FtpClient' objects>, 
    # '__weakref__': <attribute '__weakref__' of 'FtpClient' objects>, 
    # '__doc__': None, 
    # 'get': <function func_get at 0x0000000001D11EA0>}
    delattr(FtpClient, 'get')
    print(FtpClient.__dict__)
    # {'__module__': '__main__', 
    # '__init__': <function FtpClient.__init__ at 0x00000000028B50D0>, 
    # '__dict__': <attribute '__dict__' of 'FtpClient' objects>,
    # '__weakref__': <attribute '__weakref__' of 'FtpClient' objects>, 
    # '__doc__': None}
    pass

模块的动态导入

如果用from …… import *的方式,一个、两个下划线开头的函数就不能被直接导入。需要直接指定函数的名称才能被导入使用。

from m1.t import test1, _test2, __test3

test1()  # from test1
_test2()  # from test2
__test3()  # from test3

用__import__函数可以以字符串的形式导入模块。不管字符串里面写了多少层,导入的只是字符串中所表示的最顶层的模块。而且这种导入,用“点”的方式调用一个、另两个下划线开头的方法可以直接调用。

module_t = __import__('m1.t')
# 虽然写了m1.t,但是只导入了m1模块
# 调用还是要m1.t取调用
module_t.t.test1()  # from test1
module_t.t._test2()  # from test2
module_t.t.__test3()  # from test3

# 只能拿到顶级的模块名m1
print(module_t) # <module 'm1' from 'C:\\dev\\day26_object_oriented\\m1\\__init__.py'>

使用importlib模块也可以以字符串的形式来导入模块,不管字符串里面写了多少层,导入的都是最后面表示的那一层。比如,导入的的是m1.t,那么拿到的就是m1.t模块。

import importlib

mt = importlib.import_module('m1.t')
mt.test1()  # from test1
mt._test2()  # from test2
mt.__test3()  # from test3

# 可以拿到字符串指定的模块m1.t,不管多少层
print(mt)  # <module 'm1.t' from 'C:\\dev\\day26_object_oriented\\m1\\t.py'>

内置的attr方法

带双下户线的attr方法__setattr__、__getattr__、__delattr__

给属性做赋值操作(修改、增加)时,__setattr__方法被调用。

访问不存在的属性时,__getattr__方法被调用。

删除属性时,__delattr__方法被调用。

这三个方法是Python内置的,不定义也有。我们定义就相当于重新定义了这三个方法。使用者三个方法时应当注意被触发的条件。

__setattr__方法的实现不能直接用self.attr=value的直接赋值方法,因为赋值操作会触发__setattr__被调用,陷入无限递归,正确的做法是使用__dict__的字典操作,用键值对来赋值。这个看起来没什么用,事实上确实没什么用。__setattr__不常用。

__delattr__方法的实现不能用del self.item的直接删除方法,因为删除操作会触发__delattr__,陷入无限递归,正确的做法是使用__dict__的字典操作,用字典的pop方法来删除。这个看起来也没什么用,事实上确实也没什么用。__delattr__不常用。

同理__setattr__也可以重新实现。

class Foo:
    x = 10

    def __init__(self, y=27):
        self.y = y
        pass

    def __getattr__(self, item):
        print('__getattr__被调用')
        # 用来在被访问没有的属性时给出回应
        pass

    def __setattr__(self, key, value):
        print('__setattr__被调用')
        # 注意,不能直接赋值,否则会触发__setattr__陷入无限递归,报错
        # 使用__dict__做字典操作赋值
        self.__dict__[key] = value
        pass

    def __delattr__(self, item):
        print('__delattr__被调用')
        # 注意,不能直接del删除,否则会触发__delattr__陷入无限递归,报错
        self.__dict__.pop(item)
        pass

    pass


print()
# 当有属性赋值操作的时候,__setattr__被调用
f = Foo()
# __setattr__被调用
f.y = 8
# __setattr__被调用
f.z = 19
# __setattr__被调用

# 当访问对象存在的属性时,不会调用__getattr__
print(f.x)
# 10

# 当访问对象不存在的属性时,__getattr__被调用
print(f.w)
# __getattr__被调用
# None

# 当删除属性时,__delattr__被调用
del f.y
# __delattr__被调用

__getattr__方法的牛逼用法

通过对__getattr__方法的牛逼用法,实现了不实现类的所有功能,却能直接调用其组合内容的所有方法。

class FileHandler:
    def __init__(self, filename, mode='r', encoding='utf-8'):
        self.file = open(filename, mode=mode, encoding=encoding)
        self.mode = mode
        self.encoding = encoding
        pass

    def __getattr__(self, item):
        # 触发这个方法以后用getattr函数返回self.file对应是属性
        # 从而实现了将file的属性全部放给用户调用
        return getattr(self.file, item)
        pass

    pass


f = FileHandler('file', 'w+')
# 以下三个方法都没有在FileHandler里面实现
# 访问不存在的属性,触发了__getattr__方法
f.write('This is a test line.')
f.seek(0)
print(f.read())

继承方式完成包装

我们的list、tuple、dict、set等内置的数据类型并不会对数据类型做限制,一个列表里面既可以放str,也可以放tuple,还可以放数字。但是,有时候我们需要列表里面只能放入一种数据类型。这个时候,我们就要从这些内置的数据类型继承,他们也是类也可以被继承和派生。

我们就包装元素只能是str的list。本例只重写了append和insert方法,说明用法,并没有重写如__init__、extend等方法,所以并不是真正意义上的“元素只能是str”的list。

class StrList(list):
    """
    字符串列表,元素只能是字符串
    """

    def append(self, p_object):
        if type(p_object) is str:
            super().append(p_object)
        pass

    def insert(self, index, p_object):
        if type(p_object) is str:
            super().insert(index, p_object)
        pass

    pass


str_list = StrList('mysql')
print(str_list)
# ['m', 'y', 's', 'q', 'l']

str_list.append(123)
str_list.append('abc')
print(str_list)
# ['m', 'y', 's', 'q', 'l', 'abc']
# 123是数字没有别加进来

str_list.insert(3, 'hello')
str_list.insert(3, (1, 2, 3,))
print(str_list)
# ['m', 'y', 's', 'hello', 'q', 'l', 'abc']
# (1,2,3,)是tuple没有被加进来

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!