Python进阶到高级:类和对象的理解

2023年 8月 18日 26.7k 0

1、鸭子类型

“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。” 这是百科上对它的解释。

鸭子类型(duck typing)是动态类型的一种风格,鸭子类型对于 Python 编码来讲非常重要,理解它能让你真正理解什么是一切皆对象,更有助于我们理解这门语言的设计思想和实现原理,而不是仅仅浮于表面的念经 “一切皆对象”。

鸭子类型始终贯穿于 Python 代码当中,一个对象它是什么类型取决于它实现了什么协议,因此可以说 Python 是一种基于协议的编程语言。

那这些协议是什么,又有哪些协议?这里的协议,更多的时候我们称为魔法函数或魔法方法,因为它具有很多神奇的魔力,坊间因此称之为魔法函数。

在 Python 里面,所有以双下划线开头,且以双下划线结尾的函数都是魔法函数,就像 __init__ 这种,它们是 Python 语言天然自带的,不是通过某个类去继承而来的,我们也不要随意去自定义一个这样的函数,小心着魔。

魔法函数有很多,但是经常用到的也没多少,常用的一些魔法函数在后面的内容会逐步介绍到。

2、类型判断

在判断数据类型的时候常见的有两种方法:isinstancetype

isinstance("123", str)  # 返回布尔值
type("123")  # 直接返回类型

isinstance 主要用于判断对象的类型。这个好理解,不多讲。

type 可以查看类型,但它能做的远不止于此,它主要用于动态的创建类。

t = type("Mikigo", (), {"name": "huangmingqiang"})
T = t()
print(t)
print(T.name)
print(type(t))

huangmingqiang

你看,我们定义了一个类并赋值给 t,类名为 Mikigo,t 是类对象的引用,name 是其中的属性,Python 中一切都是对象,类也是对象,只不过是一种特殊的对象,是 type 的对象。

这个地方有点绕哈,你细品。

我看到网上好多讲 type 函数,准确讲 type 是一个类,只是用法像函数。在源码中:(通过 Pycharm 按住 Ctrl 点击进入)

class type(object):

    def __init__(cls, what, bases=None, dict=None): # known special case of type.__init__
        """
        type(object_or_name, bases, dict)
        type(object) -> the object's type
        type(name, bases, dict) -> a new type
        # (copied from class doc)
        """
        pass

有同学要问了,为什么源码里面有 pass,你没看错,源码里面就是写的 pass,这种实际上是由于底层是由 C 语言实现的(本文内容都是基于 CPython),一般的操作是看不到源码的,之所以能看到是因为 Pycharm 给我们提供的功能(其他编辑器不知道哈,没咋用过其他的),相当于以代码的形式看文档,所以我们看到的不是真正的源码,但是最接近于源码的源码,姑且称之为源码吧。

type 的参数说明:

  • 当 type() 只有一个参数时,其作用就是返回变量或对象的类型。
  • 当 type() 有三个参数时,其作用就是创建类对象:
    • 参数 1:what 表示类名称,字符串类型;
    • 参数 2:bases 表示继承对象(父类),元组类型,单元素使用逗号;
    • 参数 3:dict 表示属性,这里可以填写类属性、类方式、静态方法,采用字典格式,key 为属性名,value 为属性值。
    @staticmethod
    def my_static():
        print("this is static")
    
    t = type("Mikigo", (), {"name": "huangmingqiang", "static": my_static})
    T = t()
    t.static()
    T.static()
    
    this is static
    this is static
    

    这样就添加了一个静态方法,很清楚哈,关于静态方法是什么我们后面会讲到,这里只需要知道 type 创建类的方法就好了。

通过上面 type 的源码可以看到,type 是继承了 object 的,我们知道所有类的顶层类都是继承的 object,那 object 又是从哪里来的?打印看一下:

print(type(object))

好家伙,object 也是由 type 创建的,前面说了 type 继承了 object,这俩哥们儿完美闭环了,我直接好家伙,理解起来有点更绕了哈。

你也可以说 type 自己创建了自己,这里要细细的品。实际上如果你了解指针的概念,这里其实也不难理解,不就是自己指向自己嘛,所以说 type 创建了所有类,因为他连他自己都不放过,还有什么事情做不出来。

3、类变量和实例变量

(1)类变量是在类里面直接定义的变量,它可以被类对象访问和赋值,也可以被实例对象访问和赋值。

class Test:
    b = 1

    def __init__(self):  # 构造函数
        self.a = 1

T = Test()
print(T.b)
print(Test.b)
T.b = 2  # 通过实例对象赋值
print(T.b)
Test.b = 2 # 通过类对象赋值
print(Test.b)
1
1
2
2

b 是类变量,都能被访问和赋值,没问题哈。

(2)实例变量是在构造函数里面定义的变量,它可以被实例对象访问和赋值,不能被类对象访问和赋值。

class Test:
    b = 1

    def __init__(self):
        self.a = 1

T = Test()
print(T.a)
T.a = 2
print(T.a)
print(Test.a)
Test.a = 2
print(Test.a)
1
2
Traceback (most recent call last):
  File "/tmp/pycharm_project_16/123.py", line 12, in 
    print(Test.a)
AttributeError: type object 'Test' has no attribute 'a'

a 是实例变量,你看实例对象访问和赋值正常的,类对象访问就报错了。

4、类方法、静态方法和实例方法

(1)实例方法又称对象方法,是类中最常见的一种方法。

class Test:
    
    def obj_method(self):
        print("this is obj method")

实例方法参数必须传入 selfself 表示实例对象本身,实例方法的调用也必须通过实例对象来调用:

Test().obj_method()

(2)类方法

class Test:
    
    @classmethod
    def cls_method(cls):
        print("this is class method")

可以通过类对象调用,也可以通过实例对象调用。

Test.cls_method()
Test().cls_method()

注意两点:

  • 方法前面必须加装饰器 classmethod ,装饰器是 Python 中的一种语法糖,后面会讲到,记住这种固定用法,这种写法也是初代装饰器的用法。
  • 参数传入 clscls 表示类对象,但是注意不是必须的写法,写 cls 是一种约定俗成的写法,方便我们理解,也就是说这里你写 self 从语法上也是不会有问题的。这就是为什么有时候我们将一个实例方法改成类方法,直接在方法前面添加了装饰器,而没有改 self,仍然能正常执行的原因。

(3)静态方法,实际上就是普通的函数,和这个类没有任何关系,它只是进入类的名称空间。

class Test:
    
    @staticmethod
    def static_method():
        print("this is static method")

不需要传入任何参数。同样,可以通过类对象调用,也可以通过实例对象调用。

Test.static_method()
Test().static_method()

我看到一些社区大佬都表现出对静态方法的嫌弃,他们觉得既然静态方法和类没有关系,何不如在类外面写,直接写在模块里面岂不快哉。咱们不予评价,存在即合理。

5、类和实例属性的查找顺序

这里需要引入一个概念:MRO(Method Resolution Order),直译过来就是“方法查找顺序”。

大家知道类是可以继承的,子类继承了父类,子类就可以调用父类的属性和方法,那么在多继承的情况下,子类在调用父类方法时的逻辑时怎样的呢,如果多个父类中存在相同的方法,调用逻辑又是怎样的呢,这就是 MRO

Python2.3 之前的一些查找算法,比如:深度优先(deep first search)、广度优化等,对于一些菱形继承的问题都不能很好的处理。这部分内容比较多且杂,可以自己查阅资料。

Python2.3 之后,方法的查找算法都统一为叫 C3 的查找算法,升级之后的算法更加复杂,采用的特技版拓扑排序,这里也不细讲,可以自己查阅资料,我们只需要关心现在方法查找顺序是怎样的就行了。

来,这里举例说明:

class A:
    pass

class B:
    pass

class C(A, B):
    pass

print(C.__mro__)

__mro__ 可以查看方法的查找顺序。

(, , , )

可以看到,对于 C 来讲,它里面的方法查找顺序是 C — A — B,没毛病哈,很清楚。

现在升级一下继承关系,试试菱形继承:

class A:
    pass

class B(A):
    pass

class C(A):
    pass

class D(B, C):
    pass

print(D.__mro__)
(, , , , )

D 的查找顺序是 D — B — C — A

说明什么问题?我在这噼里啪啦说了这么多,到底想说啥?

想象一下,如果你在 B 和 C 里面都重载了 A 里面的一个方法,此时如果你想调用的是 C 里面的方法,实际上是无法调用的,因为根据方法的查找顺序,会先找到 B 里面的方法。

因此,重点来了:在 Python 中虽然是支持多继承的,但是在实际项目中不建议使用多继承,因为如果继承关系设计得不好,很容易造成逻辑关系的混乱,原因就是 MRO

Ruby 之父在《松本行弘的程序世界》书中,讲到三点多继承的问题:

  • 结构复杂化:如果是单一继承,一个类的父类是什么,父类的父类是什么,都很明确,因为只有单一的继承关系,然而如果是多重继承的话,一个类有多个父类,这些父类又有自己的父类,那么类之间的关系就很复杂了。
  • 优先顺序模糊:假如我有A,C类同时继承了基类,B类继承了A类,然后D类又同时继承了B和C类,所以D类继承父类的方法的顺序应该是D、B、A、C还是D、B、C、A,或者是其他的顺序,很不明确。
  • 功能冲突:因为多重继承有多个父类,所以当不同的父类中有相同的方法是就会产生冲突。如果B类和C类同时又有相同的方法时,D继承的是哪个方法就不明确了,因为存在两种可能性。

看看这是大佬说的,不是我说的。

那有同学要问了,我写的功能很复杂啊,必须要继承多个类,怎么办,难受!

实际上有一种比较流行且先进的设计模式:Mixin 混合模式,完美解决这个问题。

举个简单的例子:

class Animal:
    pass

# 大类
class Mammal(Animal):
    pass

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

class Bat(Mammal):
    pass

现在动物们没有任何技能,咱们需要给动物们增加一下技能:

class RunnableMixIn:
    def run(self):
        print('Running...')

class FlyableMixIn:
    def fly(self):
        print('Flying...')

注意 Mixin 的类功能是独立的,命名上也应该使用 MixIn 结尾,这是一种规范。

需要 Run 技能的动物:

class Dog(Mammal, RunnableMixIn):
    pass

需要 Fly 技能的动物:

class Bat(Mammal, FlyableMixIn):
    pass

有点感觉了没,Mixin 类的特点:

  • 功能独立、单一;
  • 只用于拓展子类的功能,不能影响子类的主要功能,子类也不能依赖 Mixin
  • 自身不应该进行实例化,仅用于被子类继承。

Mixin 设计思想简单讲就是:不与任何类关联,可与任何类组合。

6、破解私有属性

私有属性就是在类的内部能访问,外部不能访问。

在 Python 中没有专门的语句进行私有化,而通过在属性或方法前面加“两个下划线”实现。

举例:

class Test:

    def __init__(self):
        self.__mi = "Mikigo" 

    def __ki(self):
        print("Mikigo")
        
    def go(self):
        print(self.__mi)
Test().go()
Mikigo

你看,在类的内部访问私有属性是可以正常拿到的,方法也是一样的。

现在我们访问私有属性试试:

Test().__mi
Traceback (most recent call last):
  File "/tmp/pycharm_project_609/123.py", line 6, in 
    print(Test().__mi)
AttributeError: 'Test' object has no attribute '__mi'

从外部进行私有属性访问是不行的,人家是私有的。

Test().__ki()
Traceback (most recent call last):
  File "/tmp/pycharm_project_609/123.py", line 9, in 
    Test().__ki()
AttributeError: 'Test' object has no attribute '__ki'

私有方法也无法访问,没问题哈。

有同学要问了,我就是想访问,越是私有的我越想看,怎么才能看到别人的隐私,快说!

泄露天机了哈,这是 Python 一种很奇妙的结构化处理,为什么说是结构化处理,实际上 Python 拿到双下划线之后,对其进行了变形,在前面加了一个下划线和类名,我们通过这种方式可以访问:

print(Test()._Test__mi)
Test()._Test__ki()
Mikigo
Mikigo

你看,这样就可以正常访问了,但是既然作者不希望使用者调用这个方法,我们也尽量不要去强行使用它,强扭的瓜不甜。

所以说,从语言的角度是没有绝对的安全,任何语言都是这样,更多的是一种编程上的约束。

通常在大多数实践中,我们更倾向于使用一个下划线来表示私有属性,这不是真正的私有,而是一种更友好的编程规范,社区称之为 “受保护的”属性,它向使用着表达了这是一个私有的方法,但是你仍然可以使用它,这就是社区,这就是开源,respect~。

7、对象的自省机制

自省(introspection),即自我反省,而对象的自省实际上就是查看对象实现了哪些属性或方法。

简单讲就是,告诉别人:我是谁,我能干啥。

Python 的常用的自省函数有四个:dir()、type()、 hasattr()、isinstance()

(1)isinstance() 和 type() 前面也提到过,这里不讲了。

(2)dir() 是最为常用的一个自省函数:

引用前面的 Test 类

print(dir(Test))
['_Test__ki', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'go']

除了 _Test__kigo 方法以外,其他的方法都是魔法函数,即最开始我们提到的协议,你看随便一个对象就实现了这么多协议,是不是很神奇。

(3)hasattr() 主要用于判断对象中是否包含某个属性,返回布尔值。

print(hasattr(Test, "go"))
print(hasattr(Test, "wo"))
True
False

很简单,不多讲哈。

其他还有一些自省函数可以了解一下,偶尔用到也挺好的:

  • __doc__ 获取到文档字符串;
  • __name__ 获取对象的名称;
  • __dict__ 包含了类里可用的属性名-属性的字典;
  • __bases__ 返回父类对象的元组;但不包含继承树更上层的其他类。

8、super

super 函数是用于调用父类的一个方法。

class A:

    def mi(self):
        print("=== mi ===")

class B(A):

    def ki(self):
        super().mi()
B().ki()
=== mi ===

super 的使用方法是很简单的,但是如果涉及到多继承的情况下,就要小心处理。

准确的讲它不是调用父类的方法,而是调用的 MRO 顺序上的下一个方法。

9、上下文管理器

在讲到上下文管理器的时候,经常有同学一脸懵,然后我说 with 的时候,就会脱口而出 with open

没错,with 语句用得最多的也是这个,它是 Python 提供的一种处理资源回收的神奇方法,如果没有 with 我们可能需要多写很多代码。

大家都知道打开一个文件之后是需要关闭的,但是在操作文件的过程中很容易报错,这时候我们需要进行异常处理,要保证无论是否存在异常的情况下,文件都能正常的被关闭,我们几乎只能使用try里面的finally来处理:

f = open("test.txt", "w")
try:
    f.write(some_txt)
except:
    pass
finally:
    f.close()

如果用 with 语句处理就会很简单:

with open("test.txt", "w") as f:
    f.write(some_txt)

对比起来,哪个更好不用多说,自己品。

在《流畅的 Python》这本书里面提到:

在任何情况下,包括CPython,最好显式关闭文件;而关闭文件的最可靠方式是使用with语句,它能保证文件一定会被关闭,即使打开文件时抛出了异常也无妨。

那我们如何实现一个上下文管理器呢?

  • 基于类实现上下文管理器

要实现上下文管理器,需要实现两个魔法函数:__enter____exit__

看名称就知道了,enter 就是进入的时候要做的事情,exit 就是退出的时候要做的事情,很好记有没有。

class Context:

    def __init__(self, file_name):
        self.file_name = file_name
        self.f = None

    def __enter__(self):
        print("进入 with")
        self.f = open(self.file_name, "r")
        return self.f

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("退出 with")
        if self.f:
            self.f.close()

然后我们就可以使用 with 语句

with Context("test.txt") as f:
    print(f.read())
进入 with
我是一个测试文件
退出 with

完美哈,一个上下文管理器的类就轻松搞定。

  • 基于 contextlib 实现上下文管理器

还有种通过标准库实现上下文管理器的方法:

from contextlib import contextmanager

@contextmanager
def context_test(file_name):
    print("进入 with")
    try:
        f = open(file_name, "r")
        yield f
    finally:
        print("退出 with")
        f.close()

来用 with 玩耍一下

with context_test("test.txt") as f:
    print(f.read())
进入 with
我是一个测试文件
退出 with

利用生成器的原理,yield 之前是进入,yield 之后是退出,同样可以实现一个上下文管理器,稍微理解一下哈。

上下文管理器是 Python 提供给我们的一个非常方便且有趣的功能,经常被用在打开文件、数据库连接、网络连接、摄像头连接等场景下。如果你经常做一些固定的开始和结尾的动作,可以尝试一下。

10、装饰器

装饰器就是使用 @ 符号,像帽子一样扣在函数的头上,是 Python 中的一种语法糖。

前面讲类方法和静态方法的时候提到过,使用方法非常简单。

原理实际上就是将它所装饰的函数作为参数,最后返回这个函数。

@classmethod
def mikigo():
    print("My name is mikigo")

这样的写法等同于

def mikigo():
    print("My name is mikigo")
    
mikigo = classmethod(mikigo)

对比一下,使用装饰器可读性很高,很优雅是吧,语法糖就是给你点糖吃,让你上瘾。

定义一个装饰器

  • 不带参数的装饰器

举个例子:

def logger(func):
    def wrapper(*args, **kw):
        print('我要开始搞 {} 函数了'.format(func.__name__))
        func(*args, **kw)  # 函数执行
        print('搞完了')
    return wrapper

这是一个简单的装饰函数,用途就是在函数执行前后分别打印点日志。

有2点需要注意:

(1)装机器是一种高阶函数,在函数内层定义函数,并返回内层函数对象,多层级同理。

(2)最外层函数传入的参数是被装饰函数的函数对象。

@logger
def add(x, y):
    print('{} + {} = {}'.format(x, y, x+y))

来,试试看

add(5, 10)
我要开始搞 add 函数了
5 + 10 = 15
搞完了
  • 带参数的装饰器
from functools import wraps

def logger(say_some):
    @wraps
    def wrapper(func):
        def deco(*args, **kw):
            print("搞之前我先说两句:{}".format(say_some))
            print('我要开始搞 {} 函数了:'.format(func.__name__))
            func(*args, **kw)  # 函数执行
            print('搞完了')
        return deco
    return wrapper

你看,都是外层函数返回内层函数对象,参数放在最外层。@wraps 可加可不加,它的用途主要是保留被装饰函数的一些属性值。

@logger("别整,不得劲儿~")
def add(x, y):
    print('{} + {} = {}'.format(x, y, x+y))

执行试试

add(5, 10)
搞之前我先说两句:别整,不得劲儿~
我要开始搞 add 函数了:
5 + 10 = 15
搞完了

很奈斯,就这点儿东西。

这是最常见的实现方法,现在咱们搞点不一样的。

基于类实现装饰器

基于类装饰器的实现,必须实现 __call____init__ 两个魔法函数。

  • 不带参数的类装饰器
class logger:

    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print('我要开始搞 {} 函数了'.format(self.func.__name__))
        f = self.func(*args, **kwargs)
        print('搞完了')
        return f

不带参数的类装饰,func 是通过 init 函数里面构造的。

试试看

@logger
def add(x, y):
    print('{} + {} = {}'.format(x, y, x+y))
add(5, 10)
我要开始搞 add 函数了
5 + 10 = 15
搞完了

so easy 哈,鸭子类型,实现了装饰器协议,就是装饰器对象。

  • 带参数的类装饰器
class logger:

    def __init__(self, say_some):
        self.say_some = say_some

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            print("搞之前我先说两句:{}".format(self.say_some))
            print('我要开始搞 {} 函数了'.format(func.__name__))
            func(*args, **kwargs)
            print('搞完了')
        return wrapper

带参数的类装饰器,func 是在 call 函数里面,参数是通过 init函数传入的,这里区别比较大哈。

@logger("别整,真的不得劲儿~")
def add(x, y):
    print('{} + {} = {}'.format(x, y, x+y))
add(5, 10)
搞之前我先说两句:别整,真的不得劲儿~
我要开始搞 add 函数了
5 + 10 = 15
搞完了

这类属于装饰器的高阶用法了,在一些优秀的框架源码里面比较常见。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论