Python学习笔记之类与对象

这篇文章介绍有关 Python 类中一些常被大家忽略的知识点,帮助大家更全面的掌握 Python 中类的使用技巧

1、与类和对象相关的内置方法

  • issubclass(class, classinfo):检查 class 是否为 classinfo 的子类,classinfo 可以是一个类也可以是由多个类组成元组,注意 class 被认为是 class 的子类,也被认为是 object(所有类默认继承于object)的子类,若传入的类型与期望不符则抛出 TypeError 异常
  • isinstance(object, classinfo):检查 object 是否为 classinfo 的实例化对象,classinfo 可以是一个类也可以是由多个类组成元组,注意若 object 不是对象则返回 False,若 classinfo 既不是类也不是由多个类组成元组则抛出 TypeError 异常
  • == 比较两对象是否相等is 比较两对象是否相同
  • hasattr(object, name):检查 object 中是否有特定属性 name,注意 name 需要用双引号包围
  • getattr(object, name [, default]):获取 object 中特定属性 name 的值,若属性不存在,则抛出异常(没设置 default)或打印提示信息default(有设置 default),注意 name 需要用双引号包围
  • setattr(object, name, value):设置 object 中特定属性 name 的值,若属性不存在,则新建一个属性并赋值,注意 name 和 value 都需要用双引号包围
  • delattr(object, name):删除 object 中特定属性 name 的值,若属性不存在则抛出 AttributeError 异常,注意 name 需要用双引号包围

2、隐藏

默认情况下,Python 允许在外部直接访问对象的属性

>>> class Test:
        num = 0
        def setNum(self,num): # 修改器
            self.num = num
        def getNum(self): # 访问器
            return self.num
        
>>> obj = Test()
>>> # 我们可以直接访问对象的数据成员,而不需要通过访问器和修改器
>>> obj.num = 100
>>> obj.num
# 100 

这似乎违反了隐藏的原则,因为在 Python 中没有为私有属性提供直接的支持,而要求程序员知道在什么情况下从外部修改变量才是安全的。但是 Python 有另一种方式实现了类似于私有属性的效果

要让方法或属性成为私有的(不能从外部访问),只需要让名称以两个下划线开头即可

>>> class Test:
        __num = 0
        def setNum(self,num):
            self.num = num
        def getNum(self):
            return self.num
        
>>> obj = Test()
>>> # 此时,我们将不能直接访问对象私有的属性和方法
>>> obj.num # AttributeError
>>> # 但是,我们依然可以使用访问器和修改器访问和修改数据
>>> obj.setNum(100)
>>> obj.getNum()
# 100

其实,这只是 Python 玩的一点小把戏,在类定义中,Python 对所有以两个下划线开头的名称进行了转换,即在开头加上下划线和类名,所以对于上面的例子我们依然可以直接访问对象的私有属性

>>> obj._Test__num = 100
>>> obj._Test__num
# 100

总之,你无法禁止别人访问对象的私有方法和属性,只是以双下划线开头向对方发出了强烈的信号,希望他们不要这样做

如果你不希望名称被修改,又想让别人知道不应该从外部修改属性或方法,可以使用一个下划线开头,虽然这只是一种约定,但是还是有一些作用的,例如在使用 import 语句导入模块时,将不会导入以一个下划线开头的名称

3、参数 self

在类定义的方法中,传入的第一个参数一般为 self,self 究竟是什么呢?学过 C++ 的朋友或许很容易理解,Python 中的 self 其实就相当于 C++ 中的 this 指针,它指向调用该方法的实例化对象

在刚刚接触时,常常会出现以下错误

>>> # 常见错误:忘记在一般方法中加上 self 参数
>>> class Test:
        def __init__(self):
            self.value = 0
        def getValue(): # 错误定义,没有加上参数 self
            return value

>>> test = Test()
>>> test.getValue()
# TypeError: getValue() takes 0 positional arguments but 1 was given

>>> # 常见错误:忘记在方法中调用参数时加上 self,此时会默认该参数为局部变量
>>> class Test:
        def __init__(self):
            self.value = 0
        def getValue(self): # 正确定义
            return value # 这里返回的是局部变量

>>> test = Test()
>>> test.getValue()
# NameError: name 'value' is not defined

在一般情况下,类方法都应该加上参数 self,但是也并非全部如此,在类方法中没有加上 self 参数的方法称为静态方法,它可以直接通过类来调用,而不可以通过对象调用

>>> class Test:
        def show():
            print('static function')
            
>>> test = Test()
>>> test.show() # 不可以通过对象调用
# TypeError: show() takes 0 positional arguments but 1 was given
>>> Test.show() # 但可以通过类直接调用
# static function

另外一个不需要加上 self 参数的情况是类方法,该方法传入的第一个参数是类似于 self 的参数 cls,该方法可通过对象直接调用,但参数 cls 将自动关联到类

>>> class Test:
       def show(cls):
           print('static function')
           
>>> test = Test()
>>> test.show() # 可以通过对象直接调用
# static function

4、魔法方法

魔法方法是一种特殊的方法,它的名称有以下的格式:__name__,以两个下划线开头,以两个下划线结尾,其与多种类操作有关

(1)构造函数

Python 提供一种魔法方法 __init__(self),也被称为构造函数,该方法在创建对象时自动调用,其返回值必须为 None,主要用于初始化对象

>>> class Test:
        def __init__(self,value):
            print('__init__ is called.')
            self.value = value
        def getValue(self):
            return self.value
            
>>> obj = Test(16)
# __init__ is called.
>>> obj.getValue()
# 16

在继承机制中,方法重写对构造函数尤为重要,对于一般的子类而言,在构造函数中不仅需要父类的初始化代码,还需要自己的初始化代码。要注意,在重写构造函数时,必须调用父类的构造函数,否则可能无法正确初始化对象

>>> class Person:
        def __init__(self,name):
            self.name = name
        def show(self):
            print('Name:', self.name)
            
>>> class Student(Person):
        def __init__(self,stuID): # 构造函数重写,没有调用父类的初始化方法
            self.stuID = stuID
        def show(self): # 普通方法重写
            print('Name:', self.name)
            print('stuID:', self.stuID)
            
>>> student = Student(1234)
>>> student.show() 
# AttributeError: 'Student' object has no attribute 'name'

我们可以看到上述的使用方法是错误的,因为在子类中没有调用父类的初始化方法,父类属性不能被正确初始化,为此,有两种解决方法:一是调用未关联的父类构造函数,二是使用函数 super

>>> # 调用未关联的父类构造函数,旧式用法
>>> class Student(Person):
        def __init__(self,name,stuID): # 构造函数重写,调用父类的初始化方法
            Person.__init__(self,name)
            self.stuID = stuID
        def show(self): # 普通方法重写
            Person.show(self)
            print('stuID:', self.stuID)
            
>>> student = Student('Peter',1234)
>>> student.show()
# Name: Peter
# stuID: 1234
>>> # 使用函数 super,新式用法
>>> class Student(Person):
        def __init__(self,name,stuID): # 构造函数重写,调用父类的初始化方法
            super().__init__(name) # 区别仅仅在于这一命令
            self.stuID = stuID
        def show(self): # 普通方法重写
            super().show()
            print('stuID:', self.stuID)
            
>>> student = Student('Peter',1234)
>>> student.show()
# Name: Peter
# stuID: 1234

在上面的例子中,可以看到两种处理方法的效果是一致的,但是调用未关联的父类构造函数难以处理多继承的问题,而使用函数 super 依然可以同样的语法处理,而忽视其内部复杂的处理机制,所以在新版的 Python 中,建议使用 super 代替未关联的父类构造函数

事实上,__init__(self) 并不是实例化对象时第一个被调用的方法,第一个被调用的方法是 __new__(cls),该方法的第一个参数是 cls,若还有其它参数则原封不动地传递给 __init____new__ 方法一般返回一个实例化对象,正常情况下极少重写 __new__ 方法,但当需要继承一个不可变方法,又需要对其进行修改时,可以重写该方法

>>> class MyString(str):
        def __new__(cls,string):
            string = string.upper()
            return str.__new__(cls,string)
    
>>> obj = MyString('abcdefg')
>>> obj
# 'ABCDEFG'

(2)析构函数

另外,Python 还提供魔法方法 __del__(self),也被称为析构函数,该方法在对象销毁前自动调用,但事实上,使用 del 语句销毁对象时并不会立即调用 __del__(self)方法,而是当所有指向该对象的标签被销毁时,该方法才会被调用

>>> class Test:
        def __del__(self):
            print('__del__ is called')
            
>>> obj = Test()
>>> temp = obj
>>> del obj # 删除指向对象的标签时,并不直接调用 __del__
>>> del temp # 删除指向该对象的所有标签时,才会调用 __del__
# __del__ is called

(3)元素访问

使用本小节中介绍的魔法方法,可以帮助我们更好的创建序列和映射类型,实际上,序列和映射基本上是元素的集合,要实现它们的基本行为,不可变对象只需要实现 2 个方法(__len____getitem__),而可变对象则需要实现 4 个方法(除上述两个外,加上 __setitem____delitem__

  • __len__(self):返回集合包含的项数,对序列而言是元素个数,对映射而言是键-值对个数,当对象使用内置函数 len 时被调用 len(self)

  • __getitem__(self,key):返回与指定键相对应的值,对序列而言键应该是整数,对映射而言是键可以是任何类型,当对象被访问时调用 self[key]
  • __setitem__(self,key,value):以与键相关联的方式储存值,仅在可变对象中才需要实现,当对象被修改时调用 self[key] = value
  • __delitem__(self,key):删除与键相关联的值,仅在可变对象中才需要实现,当对象使用内置函数 del 时被调用 del self[key]

>>> class MyDic:
        def __init__(self):
            self.dic = dict()
        def __getitem__(self,key):
            print('__getitem__ is called')
            return self.dic[key]
        def __setitem__(self,key,value):
            print('__setitem__ is called')
            self.dic[key] = value
        # 这里为了演示效果,没有实现 __del__ 方法,因此不能使用 del 删除其中的元素

>>> dic = MyDic()
>>> dic['A'] = 1 # 修改器
# __setitem__ is called
>>> dic['A'] # 访问器
# __getitem__ is called
# 1
>>> del dic['A'] # AttributeError: __delitem__

除了以上最基本的魔法方法可以帮助我们创建序列和映射类型外,还有其它一些有用的魔法方法,例如,__contains__(self,item) 定义使用成员运算符 in 或者 not in 时的行为

(4)属性访问

属性访问魔法方法可以拦截对对象属性的所有访问企图,一般可以执行权限检查,日志记录等操作

  • __getattribute__(self,name):在属性被访问时自动调用
  • __getattr__(self,name):在属性被访问且对象没有该属性时自动调用
  • __setattr__(self,name,value):在试图给属性赋值时自动调用
  • __delattr__(self,name):在试图删除属性时自动调用
>>> class Test:
        def __init__(self):
            self.num = 0
        def __setattr__(self,name,value):
            print('__setattr__ is called')
            self.__dict__[name] = value
            # 注意,在这里使用 self.name = value 这样的常规赋值语句是错误的,
            # 因为在语句中会再次调用 __setattr__,导致无限循环的错误
            # 因此,可以使用对象内置属性 __dict__ 进行赋值
        def __getattribute__(self,name):
            print('__getattribute__ is called')
            return super().__getattribute__(name)
            # 同样,在这里使用 self.name 或 self.__dict__[name] 都是错误的
            # 因为在语句中会再次调用 __getattribute__,导致无限循环的错误
            # 因此,唯一安全的方法是使用 super
        def __getattr__(self,name):
            print('__getattr__ is called')
            raise AttributeError
            # 当无法找到属性时被调用,返回异常 AttributeError

            
>>> test = Test() # __init__ 被调用,执行属性 num 赋值
# __setattr__ is called
# __getattribute__ is called
>>> test.num # 执行属性 num 访问
# __getattribute__ is called
# 0
>>> test.void # 执行属性 void 访问
# __getattribute__ is called
# __getattr__ is called
# AttributeError

(5)运算符

① 算术运算

二元运算符:

  • __add__(self, other):定义加法行为:+

  • __sub__(self, other):定义减法行为:-

  • __mul__(self, other):定义乘法行为:*

  • __truediv__(self, other):定义真正除法行为:/

  • __floordiv__(self, other):定义整数除法行为://

  • __mod__(self, other):定义取模运算行为:%

  • __pow__(self, other [, modulo]):定义取幂行为:**

  • __lshift__(self, other):定义按位左移行为:<<

  • __rshift__(self, other):定义按位右移行为:>>

  • __and__(self, other):定义按位与行为:&

  • __or__(self, other):定义按位或行为:|

  • __xor__(self, other):定义按位异或行为:^

注意:

  • 反运算:在函数名前加一个字符 r,当左操作数不支持该算术运算时,则会利用右操作数调用该算术运算方法
  • 增量赋值:在函数名前加一个字符 i,支持类似于 x = y (即 x = x y)的操作

一元操作符:

  • __pos__(self):定义正号行为:+
  • __neg__(self):定义减号行为:-
  • __abs__(self):定义绝对值行为:abs()
  • __invert__(self):定义按位取反行为:~

② 比较运算

  • __lt__(self, other):定义小于行为:<
  • __le__(self, other):定义小于等于行为:<=
  • __eq__(self, other):定义等于行为:=
  • __ne__(self, other):定义不等于行为:!=
  • __gt__(self, other):定义大于行为:>
  • __ge__(self, other):定义大于等于行为:>=
>>> class Test:
        def __init__(self,value):
            self.value = value
        def __add__(self,other):
            return Test(self.value + other.value)
        def __neg__(self):
            return Test(-self.value)
        def __gt__(self,other):
            return (self.value > other.value)
        
>>> test1 = Test(100)
>>> test2 = Test(1)
>>> test1 > test2
# True
>>> test3 = test1 + (-test2)
>>> test3.value
# 99

(6)输出函数

  • __str__(self):当使用函数 str 或使用函数 print 时被调用
  • __repr__(self):当使用函数 repr 或直接输出对象时被调用
>>> class Test:
    def __init__(self,value):
        self.value = value
    def __str__(self):
        return str(self.value)
    def __repr__(self):
        return 'This is the class, Test'
    
>>> test = Test(100)
>>> str(test)
# '100'
>>> print(test)
# 100
>>> repr(test)
# 'This is the class, Test'
>>> test
# This is the class, Test

(7)描述符类

描述符类用于描述另一个类的属性,其将某种特殊类型的类实例指派给另一个类的属性,特殊类型的类需至少实现以下三个内置方法之一

  • __get__(self, instance, owner):用于访问属性,返回属性的值,self 为特殊类型的实例,instance 为另一个类的实例,owner 为另一个类的对象

  • __set__(self, instance, value):用于设置属性,不返回任何内容,self为特殊类型的实例,instance为另一个类的实例,value为传入的值

  • __delete__(self, instance):用于删除属性,不返回任何内容,self为特殊类型的实例,instance为另一个类的实例

在 Python 中有一个自带的描述符类 property,也可以直接使用

property(fget=None, fset=None, fdel=None,doc=None)

当指定 property 时,

  • 若没有指定任何参数,则创建的特性既不可读也不可写

  • 若指定一个参数( fget ),则创建的特性将是只读的
  • 若指定两个参数( fget 和 fset ),则创建的特性将是可读可写的
  • 第三个参数是可选的,指定用于删除属性的方法,这个方法不接受任何参数
  • 第四个参数也是可选的,指定一个文档字符串

>>> class MyProperty: # 自己创建一个描述符类
        def __init__(self,fget=None,fset=None,fdel=None):
            self.fget = fget
            self.fset = fset
            self.fdel = fdel
        def __get__(self,instance,onwer):
            print('__get__ is called')
            return self.fget(instance)
        def __set__(self,instance,value):
            print('__set__ is called')
            self.fset(instance,value)
        def __delete__(self,instance):
            print('__delete__ is called')
            self.fdel(instance)
            
>>> class Test:
        def __init__(self):
            self._value = 0
        def _getValue(self):
            return self._value
        def _setValue(self,value):
            self._value = value
        value = MyProperty(_getValue,_setValue)
        
>>> test = Test()
>>> test.value = 1
# __set__ is called
>>> test.value
# __get__ is called
# 1

5、迭代器

迭代意味着重复多次,我们常常使用 for 循环迭代序列和字典,但实际上我们也可以迭代实现了方法
__iter__ 的对象,一般的定义为实现了方法 __iter__ 的对象是可迭代的,而实现了方法 __next__ 的对象是迭代器

  • __iter__:返回一个迭代器,它是包含方法 __next__ 的对象,调用时可不提供任何参数
  • __next__:返回下一个值,如果迭代器没有可供返回的值,则引发 StopIteration 异常

那为什么需要使用迭代器呢?这 是因为使用迭代器在某种情形下比列表更节省内存

>>> class Fib: # 斐波那契数列
        def __init__(self):
            self.a = 0
            self.b = 1
        def __next__(self):
            self.a,self.b = self.b,self.a+self.b
            return self.a
        def __iter__(self):
            return self
        
>>> fib = Fib() # 迭代器
>>> for i in fib:
    if i < 10:
        print(i)
    else:
        break
        
1
1
2
3
5
8

另外,对可迭代对象使用内置方法 iter 也可以获得一个生成器,并且可以使用内置方法 next 获取迭代器的下一个值,如果迭代器没有可返回的值,则引发 StopIteration 异常

>>> it = iter([1,2])
>>> next(it)
# 1
>>> next(it)
# 2
>>> next(it)
# StopIteration

6、生成器

如果列表元素可以按照某种算法推算出来,那我们就可以在循环的过程中不断推算出后续的元素,而不必创建完整的列表,从而节省大量的空间,这样的机制称为生成器

相信大家都已经学过列表生成式了,最简单的一种创建生成器方法是把列表生成式中的 [] 改为 () 即可

>>> li = [i for i in range(5)]
>>> type(li)
# <class 'list'>
>>> gene = (i for i in range(5))
>>> type(gene)
# <class 'generator'>

生成器也是可迭代对象,故可以使用 for 循环进行迭代,也可以使用内置方法 next 获取下一个值

>>> next(gene)
# 0
>>> next(gene)
# 1
>>> for i in gene:
    print(i)    
# 2
# 3
# 4

另外一种创建生成式的方法是在函数中使用 yield,此时函数将不再是普通的函数,而是成为一个生成器

>>> def fib():
        a, b = 0, 1
        while True:
            yield b
            a, b = b, a + b
            
>>> f = fib()
>>> type(f)
# <class 'generator'>
>>> next(f)
1

最难理解的地方就是生成器和普通函数的执行流程不太一样

  • 普通函数是顺序执行,遇到 return 语句则返回;
  • 生成器在每次请求值时都执行生成器的代码,直至遇到 yield 或 return。yield 意味着返回一个值,且下次执行从上次返回的 yield 语句处开始,而 return 意味着生成器应停止执行

猜你喜欢

转载自www.cnblogs.com/wsmrzx/p/10301566.html