《Python面向对象编程指南》——1.12 更多的__init__()技术

简介:

本节书摘来自异步社区《Python面向对象编程指南》一书中的第1章,第1.12节,作者[美]Steven F. Lott, 张心韬 兰亮 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。

1.12 更多的__init__()技术

我们再来看一下其他一些更高级的__init__()技术的应用。相比前面的介绍,它们的应用场景不是特别常见。

以下是Player类的定义,初始化使用了两个策略对象和一个table对象。这个__init__()函数看起来不够漂亮。

class Player:
   def __init__( self, table, bet_strategy, game_strategy ):
     self.bet_strategy = bet_strategy
     self.game_strategy = game_strategy
     self.table= table
   def game( self ):
     self.table.place_bet( self.bet_strategy.bet() )
     self.hand= self.table.get_hand()
     if self.table.can_insure( self.hand ):
       if self.game_strategy.insurance( self.hand ):
         self.table.insure( self.bet_strategy.bet() )
     # Yet more... Elided for now

Player类中的__init__()函数的行为似乎仅仅是保存对象。代码逻辑只是把参数的值复制到同样名称的变量中。如果我们有很多参数,复制逻辑会显得臃肿且重复。

我们可以像如下代码这样使用这个Player类(和相关对象)。

table = Table()
flat_bet = Flat()
dumb = GameStrategy()
p = Player( table, flat_bet, dumb )
p.game()

我们可以通过把关键字参数值直接转换为内部变量,以提供一个非常短而且灵活的初始化方式。

以下是一种使用关键字参数值来创建Player类的方式。

class Player2:
   def __init__( self, **kw ):
     """Must provide table, bet_strategy, game_strategy."""
     self.__dict__.update( kw )
   def game( self ):
     self.table.place_bet( self.bet_strategy.bet() )
     self.hand= self.table.get_hand()
     if self.table.can_insure( self.hand ):
       if self.game_strategy.insurance( self.hand ):
         self.table.insure( self.bet_strategy.bet() )
     # etc.

为了换来简洁的代码,这种实现方式牺牲了大量的可读性。它使得代码意图变得模糊。

既然__init__()函数缩减到了一行,函数的很多多余的重复逻辑也被拿掉了。然而这种多余也被转化为对象各自的构造函数表达式。既然我们不再使用位置参数,那么我们就需要为对象初始化表达式提供参数名,如以下代码段所示。

p2 = Player2( table=table, bet_strategy=flat_bet, game_strategy=dumb )

为什么这样做?

这样的类设计非常容易扩展,我们几乎不用担心是否需要传入额外的参数给构造函数。

以下是调用的例子。

>>> p1= Player2( table=table, bet_strategy=flat_bet, game_
strategy=dumb)
>>>p1.game()

以下代码演示了这个设计带来的可扩展性。

>>> p2= Player2( table=table, bet_strategy=flat_bet, game_
strategy=dumb, log_name="Flat/Dumb" )
>>>p2.game()

我们添加了一个log_name属性而并不需要修改类定义,这个属性或许可以用来进行统计分析。Player2.log_name属性可以用于日志的注解或其他数据。

这里存在一个限制,我们只可以添加类内部不会发生冲突的参数名。在创建子类时需要了解类的实现,以避免关键字参数名冲突。由于**kw参数提供了很少的信息,我们不得不去知道它的实现细节。可在大多数情况下,我们要相信一个类并使用它而不是去查看它的实现细节。

这种基于关键字的初始化可以放在基类中实现,以简化子类。当新需求导致需要添加参数时,我们不必在每个子类中都实现一个__init__()函数。

这种实现方式的弊端在于存在一些变量在子类中没有提供文档说明。当仅需要添加一个变量时,可能需要改变整个类层次结构。第1个变量添加之后往往还会需要第2个和第3个。在设计的开始,我们应当考虑设计一些灵活的子类,而不是完美的基类。

我们可以(而且应该)像下面代码段那样同时使用位置变量和关键字变量。

class Player3( Player ):
   def __init__( self, table, bet_strategy, game_strategy, **extras 
):
     self.bet_strategy = bet_strategy
     self.game_strategy = game_strategy
     self.table= table
     self.__dict__.update( extras )

这种方式看起来比完全开放的定义更明智。我们把必需的参数设为位置参数,把可选参数通过关键字参数传入。这也演示了如何通过 extra关键字参数把可选参数传入__init__()函数。

这样的灵活性,基于关键字的初始化依赖于我们是否已经定义了相对透明的类。这种实现需要特别关注一下命名,因为关键字参数名是开放式的,要避免调试过程中发生命名冲突。

1.12.1 带有类型验证的初始化

需要类型验证的场景很少。从某种程度上说,这是对Python的误解。从概念上来看,类型验证是为了验证所有的参数类型是恰当的类型,而这里对“恰当”的定义往往作用不大。

这和验证对象是否符合其他标准是不同的,例如数字范围检查和防止无限循环。

在__init__()函数中实现以下逻辑可能会带来问题。

class ValidPlayer:
   def __init__( self, table, bet_strategy, game_strategy ):
     assert isinstance( table, Table )
     assert isinstance( bet_strategy, BettingStrategy )
     assert isinstance( game_strategy, GameStrategy )
     self.bet_strategy = bet_strategy
     self.game_strategy = game_strategy
     self.table= table

这里使用了isinstance()函数检查了每个类型的合法性。

我们编写了玩牌游戏模拟器并通过不断地改变GameStrategy类来进行实验。由于它们都很简单(只有4个函数),继承的好处不够凸显,我们可以单独定义每个子类而不再定义基类。

正如本例中所演示的,我们将不得不创建子类,目的只是为了通过初始化过程的错误检查,而未能从抽象基类继承到任何可用的代码。

其中一个最大的鸭子类型问题是关于数值类型的,不同的数值类型会在不同的上下文工作。试图验证参数类型也许会导致原本工作很好的一个数值类型不再工作。当试图验证时,在Python中我们有以下两种选择。

  • 为不是很广泛的集合类型加验证。一旦代码不工作了我们将会知道本该允许使用的类型被禁止了。
  • 针对相对广泛的集合类型,通常不考虑加验证,一旦代码不工作了我们将会知道我们使用了一个不允许使用的类型。

这两点基本表达了相同的意思。代码某一天可能会无效,要么是因为一个本该允许的类型被禁止了,要么是使用了被禁止的类型。


e49e63102f0e9bb309acf75b4e32cb54cb7fe24d

面临这样一个问题:为什么要限制未来潜在的使用场景?

而通常没有一个合理的理由来说明这一点。

为了不为以后的应用场景带来阻碍,可以考虑提供文档、测试和调试日志来帮助其他程序员理解哪些类型限制是可以被处理的。为了使工作量最小化,无论如何我们都必须提供文档、日志和测试用例。

以下是一段示例文档,用于说明类所需的参数。

class Player:
   def __init__( self, table, bet_strategy, game_strategy ):
     """Creates a new player associated with a table, 
      and configured with proper betting and play strategies
     :param table: an instance of :class:'Table'
     :param bet_strategy: an instance of :class:'BettingStrategy'
     :param game_strategy: an instance of :class:'GameStrategy'
     """
     self.bet_strategy = bet_strategy
     self.game_strategy = game_strategy
     self.table= table

当使用这个类时,就会从文档得知类的参数需求。可以传入任何类型。如果类型和期望的类型不兼容,那么代码将会不工作。理想情况下,我们会使用文档测试(doctest)和单元测试(unittest)来发现这些异常的场景。

1.12.2 初始化、封装和私有化

关于Python中的私有化可以概括为:大家都是成年人。

面向对象设计使得接口和实现有了很大的差别,这也是封装的意义。一个类封装了一种数据结构、一个算法和一个外部接口等,程序设计的目的是要把接口与实现分离。

然而,没有编程语言会暴露出所有设计的细节。对于Python,也是如此。

关于类设计的一个方面,这一点没有用代码演示:对象中有关私有(实现)和公有(接口)函数或属性的差异。有些编程语言只是在概念上支持私有(C++或Java是两个例子)已经很复杂了。这类语言中的访问修饰符包括了私有、保护、公有和“未指定”,可以理解为半私有。私有关键字经常被错误使用,为子类的定义带来了没必要的复杂性。

Python中私有的概念很简单,如下所示。

  • 基本都是公有。源代码随时可修改,大家都是成年人,没有什么是可以真正被隐藏的。
  • 传统上,我们会使用命名来表明哪些不是完全公有的。它们通常是容易变化的具体实现细节,然而并不存在正式的、概念上的私有。

Python中的部分函数以_命名,标记为不完全公有。help()函数通常会忽略这类函数。可以使用像Sphinx这样的工具从文档中查找出它们的命名。

Python的内部命名以__起始(和结尾)。这也是Python如何避免内部和外部应用程序发生冲突的方式。这些内部集合的命名方式完全只是参考。毕竟,没有必要在代码中试图使用__前缀来定义一个“超级私有”的属性或函数。如果这样做的话就为以后制造了一个潜在的麻烦,当新版本的Python发布并使用了同样命名的函数或属性时,就会有命名冲突。我们还有可能和新版本中的其他名称发生冲突。

Python中关于可见度的命名规则如下所示。

  • 大部分名称是公有的。
  • 以_开始的名字通常不完全公有。使用它们来命名那些经常变化的函数,这些函数通常是实现细节。
  • 以__作为前缀和后缀的函数通常是Python内部的。程序中不该使用;命名要参考编程语言的定义。

通常,Python中的命名是根据函数(或属性)的目的来定义的,并提供文档说明。通常接口函数会有说明文档以及文档测试的例子,而实现细节的函数就不必了,提供简单的说明就可以了。

对于刚接触Python的程序员,有时会对私有化不是很常用而感到惊讶。可对于已经熟悉Python的程序员也会同样惊讶于,为了不必要的私有和公有定义的顺序而浪费很多脑细胞。因为函数名和文档已经把意图描述的很明白了。

相关文章
|
5天前
|
数据采集 存储 缓存
如何使用缓存技术提升Python爬虫效率
如何使用缓存技术提升Python爬虫效率
|
13天前
|
分布式计算 大数据 数据处理
技术评测:MaxCompute MaxFrame——阿里云自研分布式计算框架的Python编程接口
随着大数据和人工智能技术的发展,数据处理的需求日益增长。阿里云推出的MaxCompute MaxFrame(简称“MaxFrame”)是一个专为Python开发者设计的分布式计算框架,它不仅支持Python编程接口,还能直接利用MaxCompute的云原生大数据计算资源和服务。本文将通过一系列最佳实践测评,探讨MaxFrame在分布式Pandas处理以及大语言模型数据处理场景中的表现,并分析其在实际工作中的应用潜力。
48 2
|
2月前
|
数据可视化 图形学 Python
在圆的外面画一个正方形:Python实现与技术解析
本文介绍了如何使用Python的`matplotlib`库绘制一个圆,并在其外部绘制一个正方形。通过计算正方形的边长和顶点坐标,实现了圆和正方形的精确对齐。代码示例详细展示了绘制过程,适合初学者学习和实践。
48 9
|
2月前
|
数据可视化 数据处理 Python
Python编程中的数据可视化技术
在Python编程中,数据可视化是一项强大的工具,它能够将复杂的数据集转化为易于理解的图形。本文将介绍如何使用matplotlib和pandas这两个流行的Python库来实现数据可视化,并展示一些实用的代码示例。通过这些示例,读者将学会如何创建各种图表,包括折线图、柱状图和散点图等,以便更好地理解和呈现数据。
|
2月前
|
存储 数据挖掘 数据处理
Python中的计票技术
本文介绍了如何使用 Python 进行计票,包括使用字典、`collections.Counter` 和 `pandas` 等方法。通过多个示例详细展示了每种方法的具体应用,帮助读者掌握计票技巧。
31 1
|
2月前
|
算法 Python
Python图论探索:从理论到实践,DFS与BFS遍历技巧让你秒变技术大牛
图论在数据结构与算法中占据重要地位,应用广泛。本文通过Python代码实现深度优先搜索(DFS)和广度优先搜索(BFS),帮助读者掌握图的遍历技巧。DFS沿路径深入搜索,BFS逐层向外扩展,两者各具优势。掌握这些技巧,为解决复杂问题打下坚实基础。
38 2
|
2月前
|
开发框架 开发者 Python
探索Python中的装饰器:技术感悟与实践
【10月更文挑战第31天】 在编程世界中,装饰器是Python中一种强大的工具,它允许我们在不修改函数代码的情况下增强函数的功能。本文将通过浅显易懂的方式,带你了解装饰器的概念、实现原理及其在实际开发中的应用。我们将一起探索如何利用装饰器简化代码、提高可读性和复用性,同时也会分享一些个人的技术感悟,帮助你更好地掌握这项技术。
36 2
|
2月前
|
开发者 Python
Python中__init__.py文件的作用
`__init__.py`文件在Python包管理中扮演着重要角色,通过标识目录为包、初始化包、控制导入行为、支持递归包结构以及定义包的命名空间,`__init__.py`文件为组织和管理Python代码提供了强大支持。理解并正确使用 `__init__.py`文件,可以帮助开发者更好地组织代码,提高代码的可维护性和可读性。
69 2
|
2月前
|
数据采集 Web App开发 iOS开发
如何利用 Python 的爬虫技术获取淘宝天猫商品的价格信息?
本文介绍了使用 Python 爬虫技术获取淘宝天猫商品价格信息的两种方法。方法一使用 Selenium 模拟浏览器操作,通过定位页面元素获取价格;方法二使用 Requests 和正则表达式直接请求页面内容并提取价格。每种方法都有详细步骤和代码示例,但需注意反爬措施和法律法规。
|
2月前
|
数据采集 API 定位技术
Python技术进阶:动态代理IP的跨境电商解决方案
Python技术进阶:动态代理IP的跨境电商解决方案