委员长 发表于 2025-5-2 20:51:13

Python面向对象编程

# 基本概念

面向对象编程 —— Object Oriented Programming,简称OOP,是一种编程思想。OOP把对象作为程序的基本单元,**一个对象不仅包含数据还包含操作数据的函数**。

### 对比面向过程与面向对象

- **面向过程编程(Procedural programming)**:把计算机程序视为**一系列子程序的集合**。为了简化程序设计,面向过程把子程序继续切分为更小的子程序,也即把大的功能分为若干小的功能进行实现,从而降低系统的复杂度,这种做法也称为**模块化(Modularity)**。
- **面向对象编程(Object-oriented programming)**:把计算机程序视为**一组对象的集合**,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。

下面以保存和打印学生成绩表为例,分别展示面向过程编程和面向对象编程的不同:

**面向过程编程**:

```python
def save_score(name, score):
    return {'name':name, 'score':score}

def print_score(std):
    print('%s: %s' % (std['name'], std['score']))

bart = save_score('Michael', 98)
lisa = save_score('Bob', 81)

print_score(bart)
print_score(lisa)
```

面向过程编程其实就是细分功能并逐步实现,这里分出了保存成绩和打印成绩两个细的功能,并分别封装成子程序(或者说函数),然后通过调用各个子程序来实现程序的最终目标(保存并打印成绩)。

**面向对象编程**:

```python
class Student(object):
    def __init__(self, name, score):
      self.name = name
      self.score = score
    def print_score(self):
      print('%s: %s' % (self.name, self.score))

bart = Student('Bart Simpson', 59)
lisa = Student('Lisa Simpson', 87)
bart.print_score()
lisa.print_score()
```

面向对象编程强调程序的主体是对象,这里我们把学生抽象成一个**类(class)**,这个类的对象拥有 `name` 和 `score` 这两个**属性(Property)**,那么保存成绩就是把学生类实例化为**对象(object)**,打印成绩就是给每个学生对象发送一个 `print_score` 的**消息(message)**,让对象自己打印自己的属性。这个发送消息的过程又称为调用对象的**方法(method)**,注意区分函数和方法。

---

### 五种编程范式的区分

这一小节是额外加上的,因为之前第4章[函数式编程](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/04%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B.md)也讲了一种编程范式,这章又引入了面向过程编程和面向对象编程的概念,所以就在这里整理一下。也可以直接查看维基词条:(https://en.wikipedia.org/wiki/Procedural_programming#Comparison_with_imperative_programming) 以及引用的文章,讲得比较细致和清晰。更详细的可以查找专门讲编程范式的书籍来浏览。

#### 面向过程编程(Procedural programming)

面向过程就是拆分和逐步实现,前一小节已经说过了,这里不再累述。

#### 命令式编程(Imperative programming)

有时候面向过程编程又称为命令式编程,两者经常混用,但它们之间还是有一点差别的。面向过程编程依赖于**块**和**域**,比方说有 `while`,`for` 等保留字;而命令式编程则没有这样的特征,一般采用 `goto` 或者分支来实现。

#### 面向对象编程(Object-oriented programming)

面向对象也在上一小节简单介绍过了,它比面向过程编程抽象程度更高,面向过程将一个编程任务划分为若干变量、数据结构和子程序的组合,而面向对象则是划分为对象,对象的行为(方法)、使用对象的数据(成员/属性)的接口。面向过程编程使用子程序去操作数据,而面向对象则把这两者结合为对象,每一个对象的方法作用在自身上。

#### 函数式编程(Functional programming)

在模块化和代码复用上,函数式编程和面向过程编程是很像的。但函数式编程中不再强调指令(赋值语句)。面向过程编写出的程序是一组指令的集合,这些指令可能会隐式地修改了一些公用的状态,而函数式编程则规避了这一点,**每一个语句都是一个表达式,只依赖于自己而不依赖外部状态**,因为我们现在所用的计算机都是基于指令运作的,所以函数式编程的效率会稍低一些,但是函数式编程一个很大的好处就是,既然每个语句都是独立的,那么就**很容易实现并行化**了。

#### 逻辑编程(Logic programming)

逻辑编程是一种我们现在比较少接触的变成范式,它关注于表达问题是什么是不是怎样解决问题。感兴趣的话可以了解一下Prolog语言。

---

### 面向对象编程的三大特点

1. **数据封装**
2. **继承**
3. **多态**

---

<br>

## 类和实例

在Python中定义类是通过 `class` 关键字完成的,像前面例子一样:

```python
class Student(object):
    pass
```

定义类的格式是 `class 类名(继承的类名)` 。 **类名一般用大写字母开头**,关于继承的知识会留在后续的章节里详述,**object类是所有类的父类**,在Python3中不写也行(既可以写作 `class Student:` 或 `class Student():`),会自动继承,详情可以看(http://stackoverflow.com/questions/4015417/python-class-inherits-object)。

定义类以后,即使没有定义构造函数和属性,我们也可以实例化对象:

```python
>>> bart = Student()
>>> bart
<__main__.Student object at 0x10a67a590>
>>> Student
<class '__main__.Student'>
```

可以看到变量bart指向的就是一个Student类的实例,**每个实例的地址是不一样的**。

与静态语言不同,Python作为动态语言,我们可以自由地给一个实例变量绑定属性:

```python
>>> bart.name = 'Bart Simpson'
>>> bart.name
'Bart Simpson'
```

**绑定的属性在定义类时无须给出**,随时都可以给一个实例绑定新属性。 但是!**这样绑定的新属性仅仅绑定在这个实例上**,别的实例是没有的! 也就是说,**同一个类的多个实例例拥有的属性可能不同**!

对于我们认为**必须绑定的属性**,可以通过定义特殊的 `__init__` 方法进行初始化,在创建实例时就进行绑定:

```python
class Student(object):

def __init__(self, name, score):
    self.name = name
    self.score = score
```

使用 `__init__` 方法要注意:

1. 第一个参数永远是 `self`,**指向创建出的实例本身**。
2. **有了 `__init__` 方法,创建实例时就不能传入空的参数,必须传入与 `__init__` 方法匹配的参数**,参数self不用传,Python解释器会自动传入。

```python
>>> bart = Student('Bart Simpson', 59)
>>> bart.name
'Bart Simpson'
>>> bart.score
59
```

### 数据封装

数据封装是面向对象编程特点之一,对于类的每个实例而言,属性访问可以通过函数来实现。 既然**实例本身拥有属性数据**,那么访问实例的数据就不需要通过外面的函数实现,可以**直接在类的内部定义访问数据的函数**。

**利用内部定义的函数,就把数据封装起来了**,这些函数和类本身是关联的,称为**类的方法**。

```python
class Student(object):

    def __init__(self, name, score):
      self.name = name
      self.score = score

    def print_score(self):
      print('%s: %s' % (self.name, self.score))
```

依然是前面的例子,可以看到在类中定义方法和外面定义的函数**唯一区别就是方法的第一个参数永远是实例变量 `self`**。其他一致,仍然可以用默认参数,可变参数,关键字参数和命名关键字参数等参数形式。和创建实例一样,调用方法时不需传入self。

对于外部,**类的方法实现细节不用了解**,只需要知道怎样调用,能返回什么就可以了。

---

### 访问限制

尽管前一节中我们把数据用方法进行了封装,但事实上,实例化对象后,我们依然可以直接通过属性名来访问一个实例的属性值,并且自由地修改属性值。要让实例的内部属性不被外部访问,只需要在属性的名称前加上两个下划线 `__` 就可以了,此时属性就转换成了**私有属性**。

```python
>>> class Student():
...   def __init__(self, name, score):
...         self.__name = name   # 绑定私有属性__name
...         self.__score = score # 绑定私有属性__score
...   def print_score(self):   # 通过方法访问私有属性
...         print(self.__name, self.__score)
...
>>> bart = Student('Bart', 98) # 创建一个Student类的实例bart
>>> bart.__name                # 由于访问限制的保护,对外部而言,实例bart是没有__name这个属性的
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'
>>> bart.__score                # 由于访问限制的保护,对外部而言,实例bart是没有__score这个属性的
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__score'
>>> bart.print_score()          # 但是通过print_score方法可以访问该实例的__name属性和__score属性
Bart 98
```

这是怎么实现的呢?其实呀,不能访问的实质是Python解释器**对外给私有变量添加了前缀** `_类名`,比如把 `__name` 会被改成 `_Student__name` 。所以我们在外部(即不是通过类的方法)访问时,`__name` 属性是不存在的,但访问 `_Student__name` 属性就可以了:

```python
>>> bart._Student__name
'Bart'
```

所以说,**即使有访问限制,外部依然可以访问和修改内部属性**,Python没有任何机制预防这一点,只有靠使用者自己注意了。

还有一点必须明白。**使用访问限制跟绑定属性是不冲突的**,所以虽然对内而言存在 `__name` 属性,但对外而言这个属性不存在,我们依然可以给实例绑定一个 `__name` 属性:

```python
>>> bart.__name = 'Alice' # 外部绑定__name属性
>>> bart.__name         # 现在外部可以访问__name属性了
'Alice'
>>> bart.print_score()    # 但对内部方法来说,__name属性依然是原来的
Bart 98
```

Python同样**没有任何机制防止我们给实例绑定一个和私有属性同名的属性**,从外部是可以访问这样绑定的属性的,但对内部方法而言,这种**绑定的赋值不会覆盖私有属性原来的值**,所以极容易出错,只有靠使用者自己注意了。

通过 `dir(bart)` 可以查看实例包含的所有变量(属性和方法):

```python
>>> dir(bart)
['_Student__name', '_Student__score', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__',
'__name', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__weakref__', 'print_score']
```

在Python中,变量名类似 `__xxx__` 的,也就是**以双下划线开头,并且以双下划线结尾**的,是**特殊变量**,特殊变量允许直接访问,**注意私有变量不要这样取名**。有时会看到以**一个下划线开头**的变量名,比如 `_name`,这样的变量外部是可以访问的,不属于访问限制,但是按照约定,这样的变量我们应**视为私有变量,不应在外部直接访问**。

---

### 获取和修改限制访问的属性

对于限制访问的属性,外部代码还是需要进行访问和修改的,我们可以在类中定义对应的get方法和set方法:

```python
>>> class Student():
...   def __init__(self, name, score):
...         self.__name = name   # 绑定私有属性__name
...         self.__score = score # 绑定私有属性__score
...   def print_score(self):   # 通过方法访问私有属性
...         print(self.__name, self.__score)
...   def get_name(self):
...         return self.__name
...   def get_score(self):
...         return self.__score
...   def set_score(self, score):
...   if 0 <= score <= 100:
...         self.__score = score
...   else:
...         raise ValueError('bad score')
```

通过类的set方法修改属性值,而不直接在外部修改有一个明显的好处,我们**可以在类的方法中对参数做检查,避免传入无效的参数**。 比如这里可以限制修改成绩时成绩的范围必须是0~100,超出就报错。

---

<br>

## 继承和多态

### 基本概念

在OOP程序设计中,当我们定义一个类的时候,可以从某个现有的类继承,新的类称为**子类(Subclass)**,被继承的类则称为**基类/父类/超类(Base class/Super class)**。比方说我们创建一个Animal类,该类有一个run方法:

```python
>>> class Animal(object):
      def run(self):
            print('Animal is running...')
```

定义一个Dog类继承Animal类,尽管我们没有为它编写任何方法,但它却可以**获得父类的全部功能**:

```python
>>> class Dog(Animal):
       pass

>>> dog=Dog()
>>> dog.run()
Animal is running...
```

我们也可以为子类增加新的方法:

```python
>>> class Dog(Animal):
...    def run(self):
...      print('Dog is running...')
...    def eat(self):
...      print('Eating meat...')
...
>>> dog.run()
Dog is running
>>> dog.eat()
Eating meat
```

这里我们除了定义一个新的 `eat` 方法之外,还定义了一个和父类方法同名的 `run` 方法,它会覆盖父类的方法,当调用子类实例的 `run` 方法时调用的就会是子类定义的 `run` 方法而不是父类的 `run` 方法了,这个特点称作**多态**。总结一下,**如果子类也定义一个和父类相同的方法,则执行时总是调用子类的方法**。

---

### 实例的数据类型

我们**定义一个类,实际就是定义了一种数据类型**,和 `list`,`str` 等没有什么区别,要判断一个变量是否属于某种数据类型可以使用 `isinstance()` 方法:

```python
>>> isinstance(dog,Animal)
True
>>> isinstance(dog,Dog)
True
```

实例化子类的对象**既属于子类数据类型也属于父类数据类型**!但是反过来就不可以,实例化父类的对象不属于子类数据类型。

---

### 多态的好处

比方说在外部编写一个函数,接收含有 `run` 方法的变量作参数:

```python
def run_twice(animal):
    animal.run()
    animal.run()
```

当传入Animal类的实例时就执行Animal类的 `run` 方法,当传入Dog类的实例时也能执行Dog类的 `run` 方法,非常方便:

```python
>>> run_twice(Animal())
Animal is running...
Animal is running...
>>> run_twice(Dog())
Dog is running...
Dog is running...
```

有了多态的特性:

1. 实现同样的功能时,不需要为每个子类都在外部重写一个函数
2. 只要一个外部函数能**接收父类实例作参数,则任何子类实例都能直接使用这个外部函数**
3. 传入的任意子类实例在调用方法时,调用的都是子类中定义的方法(如果子类没有定义的话就调用父类的)

---

### 开闭原则

所谓开闭原则,指的是**对扩展开放**:允许新增 `Animal` 类的子类;**对修改封闭**:父类 `Animal` 可以调用的外部函数其子类也能直接调用,不需要对外部函数进行修改。

多态真正的威力:**调用方只需要关注调用函数的对象,不需要关注所调用的函数内部的细节**。当我们新增一种Animal的子类时,只要确保子类的 `run()` 方法编写正确即可,无须修改要调用的函数。对任意一个对象,我们**只需要知道它属于父类类型,无需确切知道它的子类类型具体是什么**,就可以放心地调用外部函数,而函数内部调用的方法是属于Animal类、Dog类、还是Cat类,**由运行时该对象的确切类型决定**。

---

### 静态语言 VS 动态语言

对于静态语言(如:Java),如果函数需要传入 `Animal` 类型,则传入的参数必须是 `Animal` 类型或者它的子类类型,否则无法调用 `run()` 方法。

对于动态语言而言,则不一定要传入Animal类型。**只要保证传入的对象有 `run()` 方法就可以了**。 这个特性又称"**鸭子类型**",即一个对象只需要 "看起来像鸭子,能像鸭子那样走" 就可以了。

```python
class Timer(object):
    def run(self):
      print('Start...')
```

比方说这里的Timer类既不属于Animal类型也不继承自Animal类,但它的实例依然可以传入 `run_twice()` 函数并且执行自己的 `run()` 方法。

Python的 `file-like object` 就是一种鸭子类型。真正的文件对象有一个 `read()` 方法,能返回其内容。 但是只要一个类中定义有 `read()` 方法,它的实例就可以被视为 `file-like object`。许多函数接收的参数都是 `file-like object`,不一定要传入真正的文件对象,传入任何实现了 `read()` 方法的对象都可以。

---

### 小结

- 继承可以把父类的所有功能都赋予子类,这样编写子类就**不必从零做起**,子类只需要**新增自己特有的方法,把父类不合适的方法覆盖重写**就可以了。
- 动态语言的**鸭子类型**特点决定了**继承不像静态语言那样是必须的**。

---

<br>

## 获取对象信息

这一小节主要介绍给定一个对象,如何了解对象的类型以及有哪些方法。

### type函数

使用type函数获得各种变量的类型:

```python
>>> type('str')
<class 'str'>
>>> type(None)
<type(None) 'NoneType'>
>>> type(abs)
<class 'builtin_function_or_method'>
>>> animal = Animal()
>>> type(animal)
<class '__main__.Animal'>
>>> dog =Dog()
>>> type(dog)
<class '__main__.Dog'>
```

`type()` 函数返回值是参数所属的类型,而这个返回值本身则属于type类型:

```python
>>> type(type('123'))
<class 'type'>
```

可以用来进行类型判断:

```python
>>> type('123')==str
True
>>> type('123') == type(123)
False
```

导入内建的 `types` 模块还可以做更多更强大的类型判断:

```python
>>> import types
>>> def fn():
...   pass
...
>>> type(fn)==types.FunctionType         # 判断变量是否函数
True
>>> type(abs)==types.BuiltinFunctionType # 判断变量是否内建函数
True
>>> type(lambda x: x)==types.LambdaType# 判断变量是否匿名函数
True
>>> type((x for x in range(10)))==types.GeneratorType # 判断变量是否生成器
True
```

---

### isinstance函数

判断类型除了使用 `type()` 函数之外,使用 `isinstance()` 函数也能达到一样的效果:

```python
>>> isinstance('a', str)
True
>>> isinstance(123, int)
True
>>> isinstance(b'a', bytes)
True
>>> isinstance(, (list, tuple))
True
```

并且 `isinstance()` 函数的参数二还可以是一个tuple,此时 `isinstance()` 函数将**判断参数一是否属于参数二tuple中所有类型中的其中一种,只要符合其中一种则返回 `True`**。

对于类的继承关系来说,`type()` 函数不太合适,因为我们没办法知道一个子类是否属于继承自某个类,使用 `isinstance()` 函数就可以解决这个问题了。**子类的实例也是父类的实例**。

```python
>>> animal = Animal()
>>> dog =Dog()
>>> isinstance(animal, Animal)
True
>>> isinstance(animal, Dog) # 父类实例不是子类实例
False
>>> isinstance(dog, Animal) # 子类实例同时也是父类的实例
True
>>> isinstance(dog, Dog)
True
```

---

### dir函数

`dir()` 函数**返回一个对象的所有属性和方法**:

```python
>>> dir('ABC')
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__',
'__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__',
'__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs',
'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier',
'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower',
'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit',
'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper',
'zfill']
```

形如 `__xxx__` 的属性和方法都是有特殊用途的,比如 `__len__` 方法会返回对象长度,不过我们一般直接调用内建函数 `len()` 获取一个对象的长度。而事实上,`len()` 函数内部就是通过调用对象的 `__len__()` 方法来获取长度的。两种写法都可以:

```python
>>> len('ABC')
3
>>> 'ABC'.__len__()
3
```

如果自己写的类希望能用 `len()` 函数获取对象的长度,可以在定义类时实现一个 `__len__()` 方法,例如:

```python
>>> class MyDog(object):
...   def __len__(self):
...         return 100
...
>>> dog = MyDog()
>>> len(dog)
100
```

---

### hasattr函数、setattr函数、getattr函数

首先定义一个类,并创建一个该类的实例 `obj`:

```python
>>> class MyObject(object):
...   def __init__(self):
...         self.x = 9
...   def power(self):
...         return self.x * self.x
...
>>> obj = MyObject()
```

利用 `hasattr()` 函数、`setattr()` 函数、`getattr()` 函数可以分别实现**判断属性/方法是否存在**,**绑定/赋值属性/方法**,以及**获取属性值/方法**的功能:

```python
>>> hasattr(obj, 'x')   # 有属性x吗?
True
>>> obj.x               # 直接访问属性x
9
>>> setattr(obj, 'x', 10) # 为属性x设置新的值
>>> obj.x               # 直接访问属性x
10
>>> hasattr(obj, 'y')   # 有属性y吗?
False
>>> setattr(obj, 'y', 19) # 绑定一个新属性y
>>> hasattr(obj, 'y')   # 有属性y吗?
True
>>> obj.y               # 直接访问属性y
19
>>> getattr(obj, 'y')   # 获取属性y
19
```

如果试图获取不存在的属性,会抛出 `AttributeError` 的错误:

```python
>>> obj.z             # 直接访问属性z
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'z'
>>> getattr(obj, 'z') # 获取属性z
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'z'
```

特别地,我们可以**为 `getattr()` 函数传入一个额外参数表示默认值**,这样**当属性不存在时就会返回默认值**,而不是抛出错误了,但要注意,**getattr()` 并不会把这个默认值绑定到对象**:

```python
>>> getattr(obj, 'z', 404) # 获取属性z,如果不存在,返回默认值404
404
>>> obj.z                  # 没有进行绑定,所以obj仍然没有属性z
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'z'
```

`getattr()` 函数除了可以获取属性之外,也可以用来获取方法并且赋值到变量,然后再通过变量使用:

```python
>>> fn = getattr(obj, 'power') # 获取方法 power() 并赋值给变量fn
>>> fn                         # fn指向obj.power
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn()                     # 调用fn
100
>>> obj.power()                # 结果和调用obj.power是一样的
100
```

看到这里也许有一些疑问,为什么我们明明可以通过 `obj.x = 10` 的方式设置属性值,还要用 `setattr(obj, 'x', 10)` 呢? 为什么我们明明可以通过 `obj.y` 直接访问属性值,还要用 `getattr(obj, 'x')` 呢?显然后者的写法要繁琐得多。确实,前面举得例子中,我们都没有任何必要这样写,也不应该这样写。`setattr()` 函数和 `getattr()` 函数是为了一些更特别的情况而创造的,例如:

```python
>>> attrname = 'x'
>>> getattr(obj, attrname)
10
>>> obj.attrname
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'attrname'
```

当**属性名绑定在一个变量上**时,显然直接访问就没有办法使用了,但 `getattr()` 函数则不存在这方面的问题。并且前面也提到了 `getattr()` 函数允许我们**设置默认返回值**,这时直接访问无法做到的。又例如:

```python
>>> attr = {'x':9, 'y':19, 'z':29}
>>> attr.items()
dict_items([('y', 19), ('z', 29), ('x', 9)])
>>> for k,v in attr.items():
...   setattr(obj, k, v)
...
>>> for k in attr.keys():
...   getattr(obj, k)
...
19
29
9
```

我们可以非常方便地把属性名和属性值存储在一个dict里面,然后利用循环进行赋值,而无需显式地写出 `obj.x = 9`,`obj.y = 19`,和 `obj.z = 29`,当我们需要批量赋值大量属性时,好处就体现出来了。同样地,我们也可以利用循环来读取需要的每个属性的值,而无需显式地逐个属性进行访问。

当然,如果可以直接写 `sum = obj.x + obj.y` 就不要写: `sum = getattr(obj, 'x') + getattr(obj, 'y')`,属性少的时候完全没有必要给自己添麻烦,对编写和阅读代码都不友好。

最后举个使用 `hasattr()` 函数的例子,比方说读取对象fp,我们可以首先判断fp是否有 `read()` 方法,有则进行读取,无则直接返回空值:

```python
def readSomething(fp):
    if hasattr(fp, 'read'):
      return readData(fp)
    return None
```

---

<br>

## 实例属性和类属性

由于Python是动态语言,根据类创建的实例可以任意绑定属性。

要给实例绑定属性除了通过实例变量之外,也可以通过self变量来完成绑定:

```python
class Student(object):
    def __init__(self, name):
      self.name = name # 通过self变量绑定属性

s = Student('Bob')
s.score = 90             # 通过实例变量绑定属性
```

但是,如果Student类本身需要绑定一个属性呢?可以直接在类中定义属性,这种属性是**类属性**,归Student类所有:

```python
class Student(object):
    name = 'Student'
```

当我们定义了一个类属性后,这个**类属性虽然归类所有,但类的所有实例都可以访问到**:

```python
>>> class Student(object):
...   name = 'Student'
...
>>> s = Student()       # 创建实例s
>>> print(s.name)       # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
Student
>>> print(Student.name) # 打印类的name属性
Student
>>> s.name = 'Michael'# 给实例绑定name属性
>>> print(s.name)       # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
Michael
>>> print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
Student
>>> del s.name          # 删除实例的name属性
>>> print(s.name)       # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
Student
```

从上面的例子可以看出,在编写程序的时候,千万不要给实例属性和类属性设置相同的名字,因为**相同名称的实例属性将屏蔽掉类属性**。但是删除实例属性后,再使用相同的名称进行访问,返回的就会变回类属性了。
页: [1]
查看完整版本: Python面向对象编程