《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月前
|
存储 监控 API
Python实战:跨平台电商数据聚合系统的技术实现
本文介绍如何通过标准化API调用协议,实现淘宝、京东、拼多多等电商平台的商品数据自动化采集、清洗与存储。内容涵盖技术架构设计、Python代码示例及高阶应用(如价格监控系统),提供可直接落地的技术方案,帮助开发者解决多平台数据同步难题。
|
7月前
|
JSON API 开发者
天猫商品详情API接口技术解析与Python实现
天猫商品详情API(tmall.item_get)通过商品ID获取商品标题、价格、库存、图片、SKU及评价等详细信息,支持HTTP请求与JSON格式返回,适用于电商数据分析与运营。本文提供Python调用示例,实现快速接入与数据解析。
|
4月前
|
数据可视化 大数据 关系型数据库
基于python大数据技术的医疗数据分析与研究
在数字化时代,医疗数据呈爆炸式增长,涵盖患者信息、检查指标、生活方式等。大数据技术助力疾病预测、资源优化与智慧医疗发展,结合Python、MySQL与B/S架构,推动医疗系统高效实现。
|
5月前
|
数据采集 存储 XML
Python爬虫技术:从基础到实战的完整教程
最后强调: 父母法律法规限制下进行网络抓取活动; 不得侵犯他人版权隐私利益; 同时也要注意个人安全防止泄露敏感信息.
893 19
|
8月前
|
监控 大数据 API
Python 技术员实践指南:从项目落地到技术优化
本内容涵盖Python开发的实战项目、技术攻关与工程化实践,包括自动化脚本(日志分析系统)和Web后端(轻量化API服务)两大项目类型。通过使用正则表达式、Flask框架等技术,解决日志分析效率低与API服务性能优化等问题。同时深入探讨内存泄漏排查、CPU瓶颈优化,并提供团队协作规范与代码审查流程。延伸至AI、大数据及DevOps领域,如商品推荐系统、PySpark数据处理和Airflow任务编排,助力开发者全面提升从编码到架构的能力,积累高并发与大数据场景下的实战经验。
Python 技术员实践指南:从项目落地到技术优化
|
7月前
|
机器学习/深度学习 数据安全/隐私保护 计算机视觉
过三色刷脸技术,过三色刷脸技术教程,插件过人脸python分享学习
三色刷脸技术是基于RGB三通道分离的人脸特征提取方法,通过分析人脸在不同颜色通道的特征差异
|
6月前
|
数据采集 机器学习/深度学习 数据可视化
Python量化交易:结合爬虫与TA-Lib技术指标分析
Python量化交易:结合爬虫与TA-Lib技术指标分析
|
7月前
|
机器学习/深度学习 算法 API
淘宝图片搜索接口技术解析与Python实现
淘宝图片搜索接口(拍立淘)基于图像识别技术,允许用户上传商品图片查找相似或相同商品。自2014年上线以来,已服务数千万日活用户,显著提升购物体验。接口通过CNN、ANN等技术实现图像预处理、特征提取与相似度匹配,支持多种调用方式与参数设置。本文提供Python调用示例,便于开发者快速集成。
|
7月前
|
传感器 算法 数据挖掘
Python时间序列平滑技术完全指南:6种主流方法原理与实战应用
时间序列数据分析中,噪声干扰普遍存在,影响趋势提取。本文系统解析六种常用平滑技术——移动平均、EMA、Savitzky-Golay滤波器、LOESS回归、高斯滤波与卡尔曼滤波,从原理、参数配置、适用场景及优缺点多角度对比,并引入RPR指标量化平滑效果,助力方法选择与优化。
1564 0
|
7月前
|
数据采集 自然语言处理 分布式计算
大数据岗位技能需求挖掘:Python爬虫与NLP技术结合
大数据岗位技能需求挖掘:Python爬虫与NLP技术结合

推荐镜像

更多