Python 经验 - 类与对象

Python 中的一切皆对象,包括 class、function 等,具体表现在:

  • 可赋值给变量
  • 可添加到集合
  • 可作为函数参数
  • 可作为函数返回值

对象和类型

对象的特征:身份(即对象在内存中的地址,id)、类型、值

type、object、class

type:

  • 可以创建所有对象(类、函数、集合)的类,或返回对象的类型(type(Object) == type
  • type 也是对象(自身类的对象),且作为类时 type 继承于 object:type.__bases__ == (object, )

object:

  • object 是 type 的实例,type 是 object 的类:type(object) == type
  • object 是所有类的基类(xxx.__bases__),自定义类默认继承 object:object.__bases__ == ()

常见内置类型

  • None:全局只有一个
  • 数值:int、float、complex、bool
  • 迭代:for ... in ...
  • 序列:list、bytes、range、tuple、str、array
  • 映射:dict
  • 集合:set、fronzenset
  • 上下文:with
  • 其他:模块(import)、class 和 object、函数、方法、代码、object、type、elipsis、notimplemented

isinstance 与 type

使用 isinstance 与 type 都可以判断对象的类型,但更推荐使用 isinstance,会检查对象类的继承链。

class A:
    pass

class B(A):
    pass

b = B()

print(isinstance(b, B))
print(isinstance(b, A))     # b属于B类也属于A类

print(type(b) is B)         # 判读的是id,要使用is
print(type(b) is A)         # b不是A类,所以是false

魔法函数

  • 在 Python 中使类获得某种功能不必通过继承基类、实现方法,而是实现魔法函数(协议);
  • 魔法函数属于数据模型的一种特性,可以在任何对象中定义;
  • 魔法函数对 Python 原生的类型支持更好,应尽量使用原生;
  • 可迭代对象 __iter__ 会优先于 __getitem__ 且性能更好。

常用魔法函数及其实现特性

class Company(object):
    def __init__(self, employee_list):
        self.employee = employee_list

    def __getitem__(self, item):
        return self.employee[item]

    def __len__(self):
        return len(self.employee)
    
    def __str__(self):
        print("is Company")
        
    def __repr__(self):
        print("is Company(dev)")

obj = Company()

__getitem__:可遍历、切片

for i in obj:
    pass
print(obj[:2])

__len__:取长度

len(obj)

__str__:输出类信息(__repr__于交互式环境自动调用)

print(obj)

另外更多魔法函数用法见:https://rszalski.github.io/magicmethods/

类的比较操作

和其他语言(Java、C++ 的运算符重载)类似,Python 也可以通过重写魔法函数使任意类型支持运算和比较操作,下面以比较不同图形的面积大小为例:

from functools import total_ordering
from abc import abstractmethod

# 抽象基类
# 只需实现其中两种判断的魔法函数即可(自动推测出其他的比较方法)
@total_ordering
class Shape(object):

    def __init__(self, radius):
        self.radius = radius
        self.pi = 3.14

    # 定义抽象方法(抽象基类 + 抽象方法 -> 接口)
    @abstractmethod
    def area(self):     
        pass
    
    # 小于 <
    def __lt__(self, obj):
        if not isinstance(obj, Shape):
            raise TypeError("Obj is not Shape!")
        return self.area() < obj.area()

    # 等于 =
    def __eq__(self, obj):
        if not isinstance(obj, Shape):
            raise TypeError("Obj is not Shape!")
        return self.area() == obj.area()

# 长方形
class Rectangle(Shape):

    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

# 圆形
class Circle(Shape):

    def __init__(self, radius):
        self.radius = radius
        self.pi = 3.14

    def area(self):
        return self.radius ** 2 * self.pi

rectangle = Rectangle(2, 2)
circle = Circle(2)
print(rectangle > circle, rectangle == circle, rectangle < circle)    # (False, False, True)

加减乘除等运算也支持这样的操作,感兴趣的朋友可以试试。

鸭子类型

  • 变量可以指向任何类型的对象(不需要继承,也不必考虑多态);
  • 定义了相同方法(包括魔法函数)的类,可以归为同一种类型(比如 list、set 都是可迭代对象,都可以调用 extend),以相同的方式调用该方法、也可以 for 循环迭代;
  • 缺陷是作为动态语言无法在编译时发现错误。
class Cat(object):
    def say(self):
        print("i am a cat")

class Dog(object):
    def say(self):
        print("i am a fish")

class Duck(object):
    def say(self):
        print("i am a duck")
        
animal_list = [Cat, Dog, Duck]
for animal in animal_list:
    animal().say()    # 都可以“叫”,都可以当它是动物来用,而不必区分是哪种动物

抽象基类

  • 类似 Java 中的接口(定义子类共有的方法且必须被实现),不能实例化;
  • 当希望判定某个对象的类型,或强制某个子类必须实现某方法时,可以通过定义抽象基类让子类继承;
  • collections.abc 模块包含了内部定义的抽象基类,也可以直接导入 abc 自己实现;
  • 虽然提供了抽象基类功能,但还是应该尽可能重用鸭子类型。
from collections.abc import Sized    # Sized表示实现了__len__的类型

class Company(object):
    def __init__(self, employee_list):
        self.employee = employee_list
    
    def __len__(self):
        return len(self.employee)

com = Company(["b1", "b2"])

# 如果没有基类,就需要hasattr来判断对象是否存在某个方法
hasattr(com, "__len__")  

# 继承抽象基类的类,只需要根据对象其基类即可判断是否存在某方法
isinstance(com, Sized)  # Sized抽象基类肯定存在 __len__ 方法,所以可以放心调用 len

实例:

import abc

class CacheBase(metaclass=abc.ABCMeta):
    
    @abc.abstractmethod     # 如果子类没有实现该方法,则会抛出异常
    def get(self, key):     
        pass
    
    @abc.abstractmethod
    def set(self, key, value):
        pass

class RedisCache(CacheBase):
    pass

r = RedisCache()

类属性和实例属性

定义在类和实例中的变量与方法

class A:
    aa = 1
    def __init__(self, x, y):
        self.x = x
        self.y = y

a = A(2,3)

a.aa = 100      # 修改的是实例变量,不会影响类变量
print(a.x, a.y, a.aa)
print(A.aa)

b = A(3,5)
print(b.aa)

查找顺序:

  • 由下而上,先检查实例是否存在该属性,不存在再从类中查找;
  • 多继承时,采用 C3 算法在父类中查找。
class D:
    pass

class E:
    pass

class C(E):
    pass

class B(D):
    pass

class A(B, C):
    pass

print(A.__mro__)

静态方法、类方法、对象方法

class Date:
    # 构造函数
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    # 实例方法
    def tomorrow(self):
        self.day += 1

    @staticmethod
    def parse_from_string(date_str):
        year, month, day = tuple(date_str.split("-"))
        return Date(int(year), int(month), int(day))

    # 静态方法(与普通函数没什么区别,只是封装在类中,与类交互时要指定类名)
    @staticmethod
    def valid_str(date_str):
        year, month, day = tuple(date_str.split("-"))
        return int(year)>0 and (int(month) >0 and int(month)<=12) and (int(day) >0 and int(day)<=31)

    # 类方法(隐含参数为类本身,不依赖于类名)
    @classmethod
    def from_string(cls, date_str):
        year, month, day = tuple(date_str.split("-"))
        return cls(int(year), int(month), int(day))

    # 实例方法
    def __str__(self):
        return "{year}/{month}/{day}".format(year=self.year, month=self.month, day=self.day)

if __name__ == "__main__":
    new_day = Date(2018, 12, 31)
    new_day.tomorrow()
    print(new_day)

    # 2018-12-31
    date_str = "2018-12-31"
    year, month, day = tuple(date_str.split("-"))
    new_day = Date(int(year), int(month), int(day))
    print (new_day)

    # 用 staticmethod 完成初始化
    new_day = Date.parse_from_string(date_str)
    print(new_day)

    # 用 classmethod 完成初始化
    new_day = Date.from_string(date_str)
    print(new_day)
    print(Date.valid_str("2018-12-32"))

数据封装与私有属性

  • 以双下划线开头的私有属性不可以直接通过对象直接访问,而应该定义 get、set 方法进行有条件的读写;
  • 以单下划线开头的私有属性不能用 from module import * 导入,其他方面和公有属性访问方式一样;
  • 私有属性仍然可以通过 obj._classname__field 访问,所以不是绝对安全的;
  • 利用这个机制,可以解决类继承后私有变量重名的问题。
from x.class_method import Date
class User:
    def __init__(self, birthday):
        self.__birthday = birthday     

    def get_age(self):
        return 2018 - self.__birthday.year    # 返回年龄

if __name__ == "__main__":
    user = User(Date(1990,2,1))
    print(user._Student__birthday)
    print(user.__birthday)    # 报错
    print(user.get_age())

自省机制

自省是指对象创建后可以通过一定的机制查询和修改其内部结构(如运行时能够获得对象的类型、为对象添加方法)。

from x.class_method import Date

class Person:
    name = "user"

class Student(Person):
    def __init__(self, scool_name):
        self.scool_name = scool_name

if __name__ == "__main__":
    user = Student("ywh")

    print(user.__dict__)    # 通过__dict__查询实例具有的属性(C实现,性能很高)
    user.__dict__["school_addr"] = "北京市"      # 动态修改对象
    print(user.school_addr)
    print(Person.__dict__)
    print(user.name)      # 自动向上查找
    a = [1, 2]
    print(dir(a))

super 函数

  • 子类继承自父类,有时不需要在子类中执行变量赋值:self.name = name,只需要执行父类初始化方法即可;
  • super 执行顺序:MRO 顺序(而不是调用父类构造方法)
from threading import Thread
class MyThread(Thread):
    def __init__(self, name, user):
        self.user = user
        super().__init__(name=name)

class A:
    def __init__(self):
        print ("A")

class B(A):
    def __init__(self):
        print ("B")
        super().__init__()      # 获取父类,调用其初始化方法

class C(A):
    def __init__(self):
        print ("C")
        super().__init__()
        
class D(B, C):
    def __init__(self):
        print ("D")
        super(D, self).__init__()

if __name__ == "__main__":
    print(D.__mro__)
    d = D()

多继承设计(暂时没理解)

建议使用 Mixin 模式:

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