python质数列_现代化程序开发笔记(3)——多文件与模块
本系列文章以我的个人博客的搭建为线索(GitHub 仓库:Evian-Zhang/evian-blog),记录我在现代化程序设计中的一些笔记。在这篇文章中,我将对现代编程语言的多文件和模块部分进行一些介绍。
模块化编程
随着现代编程开发项目的代码量越来越大,参与开发维护的人数越来越多,模块化编程这一理念变得十分重要。就像我所说的,模块化编程实际上是一个理念,它倡导的是开发者利用各种手段,将不同作用的代码块隔离。比方说,众所周知,巫师三的两个核心功能是昆特牌功能和与女术士增进感情的功能。假设我们是简陋版巫师三的开发者,就在开发这两个功能。为了简化,假设昆特牌功能有函数playGwent, useMonsterCard,与女术士增进感情功能有函数talk, fight等。那么,任何一个懂得规划的开发者都会知道,不管使用什么编程语言,我们的代码顺序应该是
// Gwent partvoid playGwent(Person person);
void useMonsterCard(Card monsterCard);
// ... many more functions
// Sorceress partvoid talk(Sorceress sorceress);
void fight(Sorceress sorceress);
// ... many more functions
而不是
void playGwent(Person person);
void talk(Sorceress sorceress);
// ... many more functionsvoid fight(Sorceress sorceress);
void useMonsterCard(Card monsterCard);
// ... many more functions
这样把各种功能,毫不相关的函数交错放置。只有通过合理地有序组织代码,才能使代码的开发和维护变得轻松一些。试想,如果一个开发者想维护我们的这个代码,他想找到和女术士交谈的函数,与在整个项目代码中一行一行找相比,那必然是直接在女术士相应的代码部分寻找更为轻松。
总而言之,模块化实际上是一种开发分配、代码组织的理念,就是将整个项目的代码分成许多有独立功能的模块,将毫不相关的模块分开。同时,对于没有功能依赖的模块,可以多位开发者并行开发。通过模块化的措施,可以最大程度降低开发、维护的成本和时间。
多文件编程
将项目分为许多模块,由开发者并行开发,这是模块化的理念。那么实际操作中,应该如何贯彻呢?最直观的想法,就是将不同的模块归属到不同的文件、目录下去。假如我们的简陋版巫师三是用JavaScript写的,那么如果和女术士增进感情部分的代码比较少,我们直接将这部分的代码归入到sorceress.js这个文件中;如果昆特牌部分的代码比较多,放不到一个文件中,那就单独设置一个目录gwent, 在其中可能会有monster.js, play.js等多个文件。也就是说,我们的代码结构可能会是
project
├── main.js
├── sorceress.js
└── gwent
├── monster.js
└── play.js
这样的层次结构。此外,我们还可能会使用别人写的库的功能。假设我们有一个库height用于计算跳落的伤害, 其代码结构是
height
├── damage.js
└── distance.js
将代码分到不同的文件、目录中,这不仅需要开发者遵守,编程语言也需要有相应的多文件支持。也就是说,编程语言需要支持多文件编程。
此外,当编程语言支持多文件编程时,还有一个问题引刃而解了——命名冲突。比如说,我们在昆特牌功能中有一个win函数,表示比赛获胜,而在和女术士增进感情的功能中,也有一个win函数,表示获得其芳心。那么,最简单的解决方案,就是一个函数叫winGwent, 一个函数叫winSorceress. 但是,通过编程语言对多文件的支持,或者说其对多模块的支持,只要将其分属于两个模块内,那么都叫win也就没有关系了。
在讨论大多数编程语言的多文件编程支持时,会涉及到三个概念:文件级别,目录级别,以及库级别。根据我们之前的讨论,每个文件都包含一些单独的功能。而对于那些功能有联系的文件,会将其组织在同一个目录下。而有的目录具有的功能是一些辅助性的,可以复用的功能,所以有些目录会被作为一个库发布出去,供别的人使用,也就像我们这里的height库一样。在大多数编程语言的认知中,一个文件被称为一个「模块」,一个目录被称为一个「包」。因此,一个项目本身也是一个包,它是由许多文件和包组成。而组成它的包又有下一层次的文件和包组成。而一个库可能由许多包组成,也可能就是一个包。
对于库而言,我会在后面关于包管理器的文章中专门提到,这里就先只讨论模块和包。在下面具体编程语言的讨论中,模块级别和包级别是我们刚才讲的文件和目录的概念,而非语言具体的名词概念。
Python^1
Python是区分模块级别和包级别的。在Python中,一个文件就是一个模块,而一个目录则是一个包。
一个目录如果要声明自己是一个包,则必须要在目录中包含__init__.py文件。一个文件自动是一个模块,不需要声明。
如果要用Python完成我们的简陋版巫师三,那么代码结构应该为
project
├── main.py
├── sorceress.py
└── gwent
├── __init__.py
├── monster.py
└── play.py
在main.py中,需要按模块引入。同时,引入别的库的模块和引入自己的模块没有差别。引入别的模块的方法是
import sorceress
import gwent.monster
import gwent.play
import height.damage
Kotlin^2
使用Kotlin编写的Android项目依然有模块级别和包级别的概念。但是,从名词术语的角度来看,Kotlin的模块并不指单个文件,而是一个项目;Kotlin的包则指一个目录。
一个目录不需要声明,自动是一个包。一个文件需要在开头用package关键词表明自己所属的包。
如果要用Kotlin完成我们的简陋版巫师三,那么代码结构应该为
project
├── main.kt
├── sorceress.kt
└── gwent
├── monster.kt
└── play.kt
在main.kt及sorceress.kt的第一行,需要
package project
而在monster.kt和play.kt的第一行,则要
package project.gwent
在main.kt中,需要按包引入。一个文件会自动引入同一个包下的别的文件,如果要引入别的包或者库(实际上也是包),则需要
import project.gwent
import height
JavaScript/TypeScript^3
自ECMAScript 2015之后,JavaScript有了模块的概念。在JavaScript的视角下,目录仅仅是目录的作用,并没有特殊的包的作用。因此,JavaScript只有模块的概念。
一个文件如果要表明自己是一个模块,则必须有export语句。
如果要用JavaScript或TypeScript完成我们的简陋版巫师三,那么代码结构应该为
project
├── main.js
├── sorceress.js
└── gwent
├── monster.js
└── play.js
对于main.js,引入别的模块的方法是
import './sorceress.js';
import './gwent/monster.js';
import './gwent/play.js';
import 'path/to/height/damage.js';
值得注意的是,TypeScript在进行import的时候,不需要带扩展名,也就是
import './sorceress';
import './gwent/monster';
import './gwent/play';
import 'path/to/height/damage';
Swift^5
Swift没有模块级别和包级别的概念,其「模块」指的是库的概念。
如果要用Swift完成我们的简陋版巫师三,其代码结构与之前无异:
project
├── main.swift
├── sorceress.swift
└── gwent
├── monster.swift
└── play.swift
在同一个模块下(也就是我们理解的在同一个库内),所有文件都是默认导入的,我们不需要import来导入同项目下别的文件或目录。但是,需要使用import语句导入别的库,也就是Swift中的模块:
import height
Rust^6
Rust的模块系统和JavaScript相近,没有包的概念。但其目录和文件的地位是相同的,都是一个模块,而模块可以拥有子模块。
具体而言,就是Rust把「模块」和「包」的概念等同了,gwent目录实际上就是gwent模块,其下有子模块monster和play.
一个Rust的文件自动是一个模块,但需要在其父模块中声明。一个Rust的目录必须包含mod.rs作为当前模块。
如果要用Rust来完成我们的简陋版巫师三,其代码结构为
project
├── main.rs
├── sorceress.rs
└── gwent
├── mod.rs
├── monster.rs
└── play.rs
或
project
├── main.rs
├── sorceress.rs
├── gwent.rs
└── gwent
├── monster.rs
└── play.rs
在gwent/mod.rs或gwent.rs中,必须要有
mod monster;mod play;
来声明其子模块。
在main.rs中如果要想引入别的模块,需要
mod sorceress;// declare sub modulemod gwent;// declare sub moduleusegwent::monster;// use sub moduleusegwent::play;// use sub moduleuseheight::damage;// use library module
由于Rust中目录和文件的地位都是模块,所以我们也可以同时use gwent和use gwent::monster.
访问控制
使用模块化编程后,会带来更进一步的好处,就是访问控制。所谓访问控制,就是谁能对谁干什么。一个访问控制规则可以用一个三元组表示:主体,客体,访问权限。我们的生活中常常会有访问控制的存在,比如说,QQ空间中,仅好友可见,就是一种访问控制规则,表明只有主体为我的好友的人,才能对客体——我的这条说说,进行「读」这一访问权限。
在模块化编程中,访问控制就体现在我此时处在的代码块中,能否调用别的代码块中的函数。为什么要进行访问控制呢?这主要是为了贯彻封装的理念。在一个库中,有的函数也许只是作为库内的辅助函数使用,不暴露给外部,这时候就要对这些函数进行访问控制的保护。
最简单的访问控制,就是private和public. 被标记为private的代码只能被逻辑上处于同一个代码块的别的代码调用,而被标记为public的代码却能被所有的代码访问到。这一个思想作为基础,在此之上有许多的变种。
Rust^7
最符合逻辑的访问控制操作是Rust. 它将模块视作访问控制的最小单元,其的原则只有一个:如果一个模块能访问某些代码,那么它的所有子模块都能访问该代码。根据这一宗旨,Rust的访问控制实际上只分为两种:pub(in path::to::module)在适当的代码前加上这个限定符,代表当前的代码能够被指定的模块和其子模块访问。比如说以下的代码:
```rust mod A { mod B { mod C { } } }
mod D { pub(in super::A::B) struct Foo { } } ```
那么,Foo这个结构体本身位于D这个模块,但它指定B模块可以访问自己,那么总共可以访问Foo结构体的模块有B, C, D.
不加访问控制限定符不加访问控制限定符则默认为私有。私有的代码只能被当前模块和其子模块访问。比如说以下的代码:
```rust mod A { mod B { struct Foo { } mod C { } } }
mod D { } ```
那么,能够访问到Foo这个结构体的模块只有B和C.
为了方便开发者,pub(in path)会有许多的语法糖,比如说pub(crate)代表在当前crate内能访问,pub(super)代表父模块能访问,pub(self)代表只有本模块能访问,也就等同于不加访问控制限定符。
Kotlin^8
Kotlin的访问控制限定符则有4个:private, protected, internal和public. 由于Rust并不具有OOP的全部特性,所以其访问控制可以通过简单的修饰符达到完美的效果。但是,Kotlin等语言则是OOP更强的语言,所以其访问控制的修饰符也更多了一些。
首先,我们来看每个符号的定义:private对于顶层代码(即不写在class内部的代码),是仅能由本文件内部访问
对于class内的代码,仅能由该类内部访问
protected对于class内的代码,仅能由该类及其子类访问internal对于顶层代码,能由整个Kotlin语义下的模块(即我们眼中的库级别)访问
对于class内的代码,能由该Kotlin语义下的模块内,能访问该类的代码访问
public对于顶层代码,能被所有代码访问
对于class内的代码,能由能访问该类的代码访问
就像我们刚刚所说的,正是由于拥有了继承关系,Kotlin的访问限定符因此多了一个protected, 以及其他每个符号也多了class内的含义。但是,其与Rust相比,仅能表示当前文件内(即private),或当前库级别内(即internal)的访问控制,不能做到任意包路径的访问控制。
Swift^9
Swift比Kotlin多了一个访问控制限定符,共有5个:open, public, internal, fileprivate, private. 首先,我们还是先来看其定义:open能被所有代码访问,并且能被Swift语义下的模块(即我们眼中的库级别)外的类继承
public能被所有代码访问internal能被Swift语义下的模块内的代码访问
fileprivate能被当前文件内的代码访问private仅能被当前逻辑上的实体访问
这样看来,实际上和Kotlin是类似的,只不过open多了一个能不能被继承的限定。
上述的三种语言使用的是传统意义上的访问控制,我们可以看到,Rust由于没有OOP的完整特性,所以比较灵活。但虽然Kotlin和Swift不能指定路径上的访问控制,但在实际工程中,库级别和文件级别的访问控制实际已经能够胜任了。
JavaScript/TypeScript^10
JavaScript的访问控制就比较粗糙了,但也是一个很有效的策略,它的思想就是:只要我export的,你就能用;只要我没有export的,你就不能用。
我们之前讲过,JavaScript和TypeScript中一个文件就是一个模块。在JavaScript或TypeScript中,所有代码都可以被同一模块内的其他代码使用。但是,如果想让别的模块使用某些代码,则必须将相应的代码导出去。比如说,我们有下面的代码:
// foobar.ts
function foo() { }
export interface Foo { }
export function bar() { }
export default function baz() { }
那么,在别的模块中,我们可以使用
import baz, { bar, Foo } from 'foobar'
来导入相应的代码。
Python
Python是最奇葩的一种访问控制策略,它是少数的几种通过变量名来控制访问策略的语言。
首先是对于模块来说,以_开头的代码不能被import^11. 比方说,我们有如下的一个模块:
# foo.py
def bar():
print("bar")
def _baz():
print("barz")
那么,当我们在别的模块使用
from foo import *
这个语法的时候,并不会将_baz也一并导入。但是,要注意的是,如果我们单独
from foo import _baz
是可以成功的。
其次,是对于类来说,以双下划线__开头的代码被认为是不能被子类改写的^12。比方说以官方文档中的代码为例:
class Mapping:
def __init__(self, iterable):
self.items_list = []
self.__update(iterable)
def update(self, iterable):
for item in iterable:
self.items_list.append(item)
__update = update # private copy of original update() method
class MappingSubclass(Mapping):
def update(self, keys, values):
# provides new signature for update()
# but does not break __init__()
for item in zip(keys, values):
self.items_list.append(item)
那么,当我们实例化一个MappingSubclass的时候,即使子类提供了__update, 其构造函数也不会调用子类的__update。
python质数列_现代化程序开发笔记(3)——多文件与模块相关推荐
- 现代化程序开发笔记(11)——异步编程杂谈
本系列文章以我的个人博客的搭建为线索(GitHub 仓库:Evian-Zhang/evian-blog),记录我在现代化程序设计中的一些笔记.在这篇文章中,我将以我的理解从头开始梳理一遍异步编程. 从 ...
- 现代化程序开发笔记(4)——包管理工具
本系列文章以我的个人博客的搭建为线索(GitHub 仓库:Evian-Zhang/evian-blog),记录我在现代化程序设计中的一些笔记.在这篇文章中,我会就项目构建工具和包管理工具做一些讨论,先 ...
- 微信小程序开发笔记(1.1)滚动选择器picker的使用
微信小程序开发笔记(1.1)滚动选择器picker的使用 前言 滚动选择器picker 普通选择器 多列选择器 时间选择器 日期选择器 省市区选择器 前言 最近被拉来做小程序,因为时间比较赶,其他方面 ...
- 微信小程序开发笔记,你收藏了吗?
** 微信小程序开发笔记,你收藏了吗? ** 最近在开发微信小程序,把自己在项目中经常遇到的知识点记录下来,以便下次开发的时候查看. 开发小程序开发工具推荐vscode写代码,微信开发工具用于查看效果 ...
- 微信小程序开发笔记 进阶篇④——getPhoneNumber 获取用户手机号码(小程序云)
文章目录 一.前言 二.前端代码wxml 三.前端代码js 四.云函数 五.程序流程 一.前言 微信小程序开发笔记--导读 大部分微信小程序开发者都会有这样的需求:获取小程序用户的手机号码. 但是,因 ...
- 微信小程序开发笔记 进阶篇⑤——getPhoneNumber 获取用户手机号码(基础库 2.21.2 之前)
文章目录 一.前言 二.前端代码wxml 三.前端代码js 四.后端java 五.程序流程 六.参考 一.前言 微信小程序开发笔记--导读 大部分微信小程序开发者都会有这样的需求:获取小程序用户的手机 ...
- 微信小程序开发笔记二(WXSS和CSS样式美化)
微信小程序开发笔记二(WXSS和CSS样式美化) 一.CSS基本知识 1.Class选择器的定义 2.ID选择器的定义 3.ID选择器和class选择器的区别 4.CSS中设置颜色 5.CSS中的文本 ...
- 微信小程序开发笔记——wsdchong
微信小程序开发笔记 一.小程序简介 小程序起源于微信的webview:此类API最初是提供给腾讯内部一些业务使用,很多外部开发者发现后,照葫芦画瓢,逐渐成为微信中网页的事实标准.2015年初,微信发布 ...
- 微信小程序开发笔记 进阶篇③——onfire.js事件订阅和发布在微信小程序中的使用
文章目录 一.前言 二.onfire.js介绍 三.API介绍 四.实例应用 五.onfire源码 六.实例源码 一.前言 微信小程序开发笔记--导读 二.onfire.js介绍 一个简单实用的事件订 ...
最新文章
- ASP连接Access2013
- SQLServer数据库自增长标识列的更新修改操作
- jyphon 环境变量配置
- Spring Cache抽象-缓存注解
- 分布式架构的分布式文件系统
- 【机器学习】机器学习一些概念的整理(不断更新中)
- html 如何去除浮动,CSS浮动? 如何清除浮动?
- ios macos_设计师可以从iOS 14和macOS Big Sur中学到什么?
- IntelliJ IDEA2017 激活方法 最新的
- 剑灵傲雪区最新服务器,12.8日势力优化具体内容 各大区服务器互通情况
- html5可以用flash,HTML5网页可以直接看视频,不用flash吗,另外WP7为何不支持flash。。。HTML5网页...
- 【英语学习】【WOTD】opusculum 释义/词源/示例
- 解决python在pycharm中可以import本地文件,但命令行运行时报错:no model named xxxx本地文件
- php读写文件要加锁
- express 模板 及 文件上传
- oracle关键字作为字段名使用方法
- SPI协议通信时序详解
- Hadoop环境搭建(6) -- 克隆
- prepareStatement的批量处理数据
- java提取一个字符串中的整数和小数部分
热门文章
- mpvue template compiler 中文版教程
- windows下安装composer抛出Composer\Downloader\TransportException异常解决办法
- 《税的真相》—— 读后总结
- 浅谈Junit测试中反射和Jmock的应用
- Java后台请求远程链接
- SCCM 2007 R2部署、操作详解系列之部署篇
- 百度地图API地理位置和坐标转换
- SOAP消息机制简介
- nginx最大并发连接数的思考:worker_processes、worker_connections、worker_rlimit_nofile
- Laravel自定义验证规则的实例与框架使用正则实例