搜索文章

输入标题或内容中的关键词。

← 回到技术手记
编程

Python 学习笔记(三):异常、模块与类

gemhack··23 分钟阅读

本文整理 Python 3 的相关学习笔记,示例按各小节说明理解。

异常

即便 Python 程序的语法是正确的,在运行它的时候,也有可能发生错误。运行期检测到的错误被称为异常。大多数的异常都不会被程序处理,都以错误信息的形式打印在控制台。

对于异常而言,如果程序员没有对其进行处理,则程序在遇到异常时会终止程序的运行并报错,如果程序员对异常进行捕获,则会按照程序员给定的逻辑继续执行,可以使用异常处理语句对异常进行捕获和处理。

1.捕获异常

try:
    # 执行代码
except:# 可以使用 except TypeError as e:
    # 如果遇到异常时执行的代码
else:
    # 未发生异常时执行的代码,可以没有这个分支
finally:
    # 无论发不发生异常都会执行,可以没有这个分支

捕获异常可以捕获具体的异常,使用except TypeError as e:的语法格式,当遇到对应的异常时才会捕获,如果发生其他异常则会抛出,中断程序执行。捕获的具体异常可以有多个,使用()括起来,如:

try:
    print(1/0)
except (ZeroDivisionError,NameError,TypeError) as e:# 发生除数为0,类型错误,未定义变量的异常都会被捕获。
    print(e)

2.抛出异常

python使用raise语句来抛出异常,抛出的异常对象必须是Exception的子类或者是Exception的示例对象。

# raise Exception('发生异常')
# 例如:
try:
    value = 5 / name
    if name > 5:
        raise Exception('name>')
except Exception as e:
    print(e)
    raise e# 可以选择继续向上抛出异常

异常具有继承性,会层层抛出,如果发生了异常,程序员可以选择捕获并处理或者向上抛出,如果主进程发生了异常而没有被处理,则程序会中断运行。

模块

模块是一个包含所有你定义的函数和变量的文件,其后缀名是.py。模块可以被别的程序引入,以使用该模块中的函数等功能。这也是使用 python 标准库的方法。

1.模块的导入

[from module_name] import  (类名|方法名|变量名|模块名|*) [as 别名]

当解释器遇到 import 语句,如果模块在当前的搜索路径就会被导入。

搜索路径是由一系列目录名组成的,Python 解释器就依次从这些目录中去寻找所引入的模块。搜索路径是在 Python 编译或安装的时候确定的,安装新的库应该也会修改。搜索路径被存储在 sys 模块中的 path 变量,

import sys
print(sys.path)
'''
# 示例:sys.path 包含项目目录、标准库目录和 site-packages,具体路径因环境而异。

'''

在一个程序之中,一个模块只能被导入一次,防止被导入模块被一遍又一遍地执行。

2.导入相关的模块属性

导入机制会在加载期间会根据模块的规格说明填充每个模块对象的这些属性,并在加载器执行模块之前完成。

  • _name_ __name__ 属性必须被设为模块的完整限定名称。 此名称被用来在导入系统中唯一地标识模块。
  • _package_ 模块的 __package__ 属性可以被设置。 其值必须为一个字符串,但可以与 __name__ 取相同的值。 当模块是一个包时,其 __package__ 值应当设为其 __name__ 值。 当模块不是包时,对于最高层级模块 __package__ 应当设为空字符串,对于子模块则应当设为其父包名。
  • _spec_ __spec__ 属性必须设为在导入模块时要使用的模块规格说明。 对 __spec__ 的正确设定将同时作用于 解释器启动期间初始化的模块。 唯一的例外是 __main__,其中的 __spec__ 会 在某些情况下设为 None. 当 __spec__.parent 未设置时,将使用 __package__ 作用回退项。 Added in version 3.4. 在 3.6 版本发生变更: 当 __package__ 未定义时,__spec__.parent 会被用作回退项。
  • _path_ 如果模块为包(不论是正规包还是命名空间包),则必须设置模块对象的 __path__ 属性。 属性值必须为可迭代对象,但如果 __path__ 没有进一步的用处则可以为空。 如果 __path__ 不为空,则在迭代时它应该产生字符串。 不是包的模块不应该具有 __path__ 属性。
  • _file_ __file__ 是可选项(如果设置,其值必须为字符串)。 它指明要载入的模块所在文件的路径(如果是从文件载入),或者对于从共享库动态载入的扩展模块来说则是共享库文件的路径。

Python 的特殊名 __main__ 用于两个重要的构造:

  1. 程序的顶层环境的名称,可用表达式 __name__ == '__main__' 来检查;以及
  2. Python 包中的文件 __main__.py。

1._name_ == ‘__main__’

当一个 Python 模块或包被导入时,__name__ 被设为模块的名称——通常为 Python 文件本身的名称去掉 .py 后缀:

>>>import configparser
configparser.__name__
>>>'configparser'

如果文件是包的一部分,则 __name__ 还将包括父包的路径:

>>>from concurrent.futures import process
process.__name__
>>>'concurrent.futures.process'

而若模块是在顶层代码环境中执行的,则其 __name__ 被设为字符串 '__main__'。

顶层代码环境:

__main__ 是顶层代码运行环境的名称。“顶层代码”是指由用户指定的最先开始运行的那一个 Python 模块。之所以它是“顶层”,是因为它将导入程序所需的所有其它模块。有时“顶层代码”被称为应用程序的 入口点。

惯用法

有些模块包含了仅供脚本使用的代码,比如解析命令行参数或从标准输入获取数据。 如果这样的模块被从不同的模块中导入,例如为了单元测试,脚本代码也会无意中执行。

这就是 if __name__ == '__main__' 代码块的用武之地。除非模块在顶层环境中被执行,否则该块内的代码不会运行。

将尽可能少的语句放在位于 if __name__ == '__main__' 之下的代码块中可以提高代码的清晰度和准确度。 通常,将由一个名为 main 的函数来封装程序的主要行为:

# echo.py
import shlex
import sys

def echo(phrase: str) -> None:
   """A dummy wrapper around print."""
   # for demonstration purposes, you can imagine that there is some
   # valuable and reusable logic inside this function
   print(phrase)

def main() -> int:
    """Echo the input arguments to standard output"""
    phrase = shlex.join(sys.argv)
    echo(phrase)
    return 0

if __name__ == '__main__':
    sys.exit(main())  # next section explains the use of sys.exit

请注意,如果模块没有将代码封装在 main 函数内,而是直接放在 if __name__ == '__main__' 块内,那么这个 phrase 变量对整个模块来说就是全局变量。 这很容易出错,因为模块内的其他函数可能会无意中使用全局变量而不是局部名称。 一个 main 函数解决了这个问题。

使用 main 函数有一个额外的好处,就是 echo 函数本身是孤立的,可以在其他地方导入。当 echo.py 被导入时,echo 和 main 函数将被定义,但它们都不会被调用,因为 __name__ != '__main__' 。

2.__main__.py

最常见的是, __main__.py 文件被用来为一个包提供命令行接口。假设有下面这个虚构的包,”bandclass”:

bandclass
  ├── __init__.py
  ├── __main__.py
  └── student.py

当使用 -m 标志从命令行直接调用软件包本身时,将执行 __main__.py 。比如说。

python -m bandclass

这个命令将导致 __main__.py 的运行。你如何利用这一机制将取决于你所编写的软件包的性质,但在这个假设的案例中,允许教师搜索学生可能是有意义的:

# bandclass/__main__.py

import sys
from .student import search_students

student_name = sys.argv[1] if len(sys.argv) >= 2 else ''
print(f'Found student: {search_students(student_name)}')

注意, from .student import search_students 是一个相对导入的例子。 这种导入方式可以在引用一个包内的模块时使用。

惯用法

__main__.py 的内容通常不会用 if __name__ == '__main__' 块围起来。 相反,这些文件会保持简短并从其他模块导入函数来执行。 这样其他模块就可以很容易地进行单元测试并可以适当地重用。

如果使用,一个 if __name__ == '__main__' 区块仍然会像预期的那样对包内的 __main__.py 文件起作用,因为如果导入,它的 __name__ 属性将包括包的路径:

>>>import asyncio.__main__
asyncio.__main__.__name__
>>>'asyncio.__main__'

但这对 .zip 文件的根目录中的 __main__.py 文件不起作用。 因此,为了保持一致性,建议使用不带 __name__ 检测的最小化 __main__.py。

3.自定义模块

python中有很多内置的模块,也就是标准库,除此之外还可以自定义模块。模块也是一个普通的python文件,以.py结尾,其中包含了类,方法和变量,**也可以包含代码。**模块名就是.py文件的文件名,所以应当遵循文件的命名规范。

当在其他python文件中导入模块时,使用import直接导入即可,需要注意的是,导入模块时,如果模块中有可执行的代码,就会立即执行。类似于php中的文件包含。

4.包

Python 只有一种模块对象类型,所有模块都属于该类型, 为了帮助组织模块并提供名称层次结构,Python 还引入了 包的概念。

你可以把包看成是文件系统中的目录,并把模块看成是目录中的文件,但请不要对这个类比做过于字面的理解,因为包和模块不是必须来自于文件系统。 为了方便理解本文档,我们将继续使用这种目录和文件的类比。 与文件系统一样,包通过层次结构进行组织,在包之内除了一般的模块,还可以有子包。

要注意的一个重点概念是所有包都是模块,但并非所有模块都是包。 或者换句话说,包只是一种特殊的模块。 特别地,任何具有 __path__ 属性的模块都会被当作是包。

所有模块都有自己的名字。 子包名与其父包名会以点号分隔,与 Python 的标准属性访问语法一致。 因此你可能会有一个名为 email 的包,这个包中又有一个名为 email.mime的子包以及该子包中的名为 email.mime.text的子包。

所以包有两个特点:可以组织模块和子包。普通包通常包含 __init__.py,命名空间包可以没有它;__main__.py 是通过 python -m 执行包的入口,并不是包的必需文件。

5.第三方包的导入

也就是说包是一个实现某一类功能的各种模块的集合,拓展了python的用途。所以第三方开发了很多非官方的包,能帮助我们实现各种类型的功能,比如用于科学计算的numpy包,用于数据可视化的matplotlib包还有用于网络请求的requests包等。

由于这些包是第三方的,所以需要先下载。python官方提供了pip工具用于下载这些第三方包。使用命令pip install package_name即可通过网络下载相应的第三方包,然后使用import语句导入即可使用。

类与对象

1.类定义语法

最简单的类定义形式如下:

class ClassName:
    '''这是一个自定义类''' # 类内注释文档,会自动绑定为__doc__属性的值
    <statement-1>
    .
    .
    .
    <statement-N>

与函数定义(def 语句)一样,类定义必须先执行才能生效。把类定义放在 if 语句的分支里或函数内部试试。在实践中,类定义内的语句通常都是函数定义,但也可以是其他语句。

当进入类定义时,将创建一个新的命名空间,并将其用作局部作用域 — 因此,所有对局部变量的赋值都是在这个新命名空间之内。 特别的,函数定义会绑定到这里的新函数名称。当 (从结尾处) 正常离开类定义时,将创建一个 类对象。 这基本上是一个围绕类定义所创建的命名空间的包装器;

1.属性的定义

公有属性:在类命名空间中不加任何修饰定义的属性均为公有属性,且为类属性,为每一个实例对象所共有的。任何示例都可以访问和修

改。还可以通过class_name.attr_name的方式在外部直接访问和修改。

如果要声明实例属性则是需要在属性前加上具体的实例名称,如果在类中定义则需要加上self

class Myclass:
    '''测试类'''
    classattr = 'foreveryobject'
    def __init__(self,name):
        self.name = name

o = Myclass("lisi")
print(o.classattr)# 可以通过实例对象访问类属性
print(Myclass.classattr)# 也可以通过类名访问类属性
# 输出结果均为 foreveryobject
print(o.name)# 通过实例对象调用实例属性
print(Myclass.name)# 但是无法通过类对象访问实例属性
'''
lisi
Traceback (most recent call last):
  File "/path/to/example.py", line 11, in <module>
    print(Myclass.name)
          ^^^^^^^^^^^^
AttributeError: type object 'Myclass' has no attribute 'name'
'''

在类定义结束之后,也可以通过类名.的方式后续再添加或者删除类属性,也可以通过实例添加或者删除实例属性。

o.sex =  1
Myclass.age = 19
print(o.sex)
print(o.age)# or Myclass.age
# 1
# 19

除此之外,你也可以使用以下函数的方式来访问属性:

  • getattr(obj, name[, default]) : 访问对象的属性。
  • hasattr(obj,name) : 检查是否存在一个属性。
  • setattr(obj,name,value) : 设置一个属性。如果属性不存在,会创建一个新属性。
  • delattr(obj, name) : 删除属性。

需要注意的是,如果同样的属性名称同时出现在实例和类中,则属性查找会优先选择实例:

o.age = 18
print(o.age)
# 18 not 19

**补充:**数据属性可以被方法以及一个对象的普通用户(“客户端”)所引用。 换句话说,类不能用于实现纯抽象数据类型。 实际上,在 Python 中没有任何东西能强制隐藏数据 — 它是完全基于约定的。 (而在另一方面,用 C 语言编写的 Python 实现则可以完全隐藏实现细节,并在必要时控制对象的访问;此特性可以通过用 C 编写 Python 扩展来使用。)

客户端应当谨慎地使用数据属性 — 客户端可能通过直接操作数据属性的方式破坏由方法所维护的固定变量。 请注意客户端可以向一个实例对象添加他们自己的数据属性而不会影响方法的可用性,只要保证避免名称冲突 — 再次提醒,在此使用命名约定可以省去许多令人头痛的麻烦。

2.方法的定义

类的方法与普通的函数只有一个特别的区别——它们必须有一个额外的第一个参数名称, 按照惯例它的名称是 self。self 代表的是类的实例,代表当前对象的地址,而 self._class_ 则指向类。

self 不是 python 关键字,我们把他换成 runoob 也是可以正常执行的:

class Test:
    name = 'a'
    def pri(self):
        print(self)
        print(self.__class__)
t = Test()
t.pri()
'''
<__main__.Test object at 0x000001ADC971B530>
<class '__main__.Test'>
'''

方法可以通过使用 self 参数的方法属性调用其他方法:

class Bag:
    def __init__(self):
        self.data = []

    def add(self, x):
        self.data.append(x)

    def addtwice(self, x):
        self.add(x)
        self.add(x)

方法可以通过与普通函数相同的方式引用全局名称。与方法相关联的全局作用域就是包含该方法的定义语句的模块。(类永远不会被用作全局作用域。)尽管一个人很少会有好的理由在方法中使用全局作用域中的数据,全局作用域依然存在许多合理的使用场景:举个例子,导入到全局作用域的函数和模块可以被方法所使用,定义在全局作用域中的函数和类也一样。通常,包含该方法的类本身就定义在全局作用域中,而在下一节中我们将会发现,为何有些时候方法需要引用其所属类。

每个值都是一个对象,因此具有 类 (也称为 类型),并存储为 object.__class__ 。

方法对象:

class Myclass:
    def f(self):
        return "hello world"

通常,方法在绑定后立即被调用:

x = Myclass()
x.f()

在 MyClass 示例中,这将返回字符串 'hello world'。 但是,方法并不是必须立即调用: x.f 是一个方法对象,它可以被保存起来以后再调用。 例如:

xf = x.f
while True:
    print(xf())

将持续打印 hello world,直到结束。

当一个方法被调用时究竟会发生什么? 你可能已经注意到尽管 f() 的函数定义指定了一个参数,但上面调用 x.f() 时却没有带参数。 这个参数发生了什么事? 当一个需要参数的函数在不附带任何参数的情况下被调用时 Python 肯定会引发异常 — 即使参数实际上没有被使用…

实际上,你可能已经猜到了答案:方法的特殊之处就在于实例对象会作为函数的第一个参数被传入。 在我们的示例中,调用 x.f() 其实就相当于 MyClass.f(x)。 总之,调用一个具有 n 个参数的方法就相当于调用再多一个参数的对应函数,这个参数值为方法所属实例对象,位置在其他参数之前。

总而言之,方法的运作方式如下。 当一个实例的非数据属性被引用时,将搜索该实例所属的类。 如果名称表示一个属于函数对象的有效类属性,则指向实例对象和函数对象的引用将被打包为一个方法对象。 当传入一个参数列表调用该方法对象时,将基于实例对象和参数列表构造一个新的参数列表,并传入这个新参数列表调用相应的函数对象。

2.class对象

类对象支持两种操作:属性引用和实例化。

属性引用 使用 Python 中所有属性引用所使用的标准语法: obj.name。 有效的属性名称是类对象被创建时存在于类命名空间中的所有名称。 因此,如果类定义是这样的:

class MyClass:
    """A simple example class"""
    i = 12345 # 类属性的定义语法

    def f(self): # 类方法的定义语法
        return 'hello world'

那么 MyClass.i 和 MyClass.f 就是有效的属性引用,将分别返回一个整数和一个函数对象。 类属性也可以被赋值,因此可以通过赋值来更改 MyClass.i 的值。 __doc__ 也是一个有效的属性,将返回所属类的文档字符串: "A simple example class"。

3.实例对象

类的 实例化 使用函数表示法。 可以把类对象视为是返回该类的一个新实例的不带参数的函数。 举例来说(假设使用上述的类):

x = MyClass()

创建类的新 实例 并将此对象分配给局部变量 x。

实例化操作 (“调用”类对象) 会创建一个空对象。 许多类都希望创建的对象实例是根据特定初始状态定制的。 因此一个类可能会定义名为 __init__() 的特殊方法,就像这样:

def __init__(self):
    self.data = []

当一个类定义了 __init__() 方法时,类的实例化会自动为新创建的类实例发起调用 __init__()。 因此在这个例子中,可以通过以下语句获得一个已初始化的新实例:

x = MyClass()

当然,__init__() 方法还有一些参数用于实现更高的灵活性。 在这种情况下,提供给类实例化运算符的参数将被传递给 __init__()。 例如,

>>>

>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)

参考资料

评论发表评论

评论

发表评论

邮箱不会公开。标有 * 的项目为必填项。