python - 在没有协同例程的情况下编写良好的 Scala(包括使用 Yield 的 Python 示例)

标签 python scala yield coroutine lazy-sequences

我目前正在学习 Scala,并正在寻找一种优雅的解决方案来解决一个可以通过使用协同例程轻松解决的问题。

由于 Scala 中默认情况下未启用协同例程,因此我认为它们至少不是被广泛接受的最佳实践,因此希望在不使用它们的情况下编写我的代码。

一个令人信服的论点表明协同例程/延续是最佳实践,这将是一个可接受的替代答案。

generate_all_matching_paths函数

我想编写一个在基本目录中搜索文件的函数。 匹配和下降标准应由具有“PathMatcher”特征的类的实例提供。 (至少我认为这是 Scala 的发展方向)

PathMatcher 可用于确定 fs_item_path 是否匹配,并确定搜索是否应深入到目录(如果 fs_item_path 是目录的路径)。

我现在采用的 Python 实现方法仅用于表明我想要的功能。

我想用“Scala 方式”编写这段代码。

我的目标是寻找具有以下特征的解决方案:

  • 可读
  • 在适合的地方使用 Scala 习惯用法
  • 在搜索期间(而不是之后)返回匹配的路径
  • 适用于非常大的文件集合(但不是非常深的嵌套) - 即目录中的数千个文件;总共有数百万个文件

我认为解决方案将涉及延迟评估流,但我无法以有效的方式组装流。

我还了解到,如果使用不当,惰性流可以保留“旧值”的副本。我所追求的解决方案不会这样做。

参数

基本绝对路径

开始搜索的目录的绝对路径

rel_ancestor_dir_list

目录名称列表,指示我们已下降到 base_abs_path 子目录的深度

rel_path_matcher

具有 PathMatcher 特征的类的实例。

在下面的示例中,我使用了正则表达式实现,但我不想将使用限制为正则表达式。

Python 示例

这里是一个完整的工作 Python 程序(使用 Python 3.4 测试),其中包含“generate_all_matching_paths”的 Python 版本。

程序将搜索“d:\Projects”以查找以“json”结尾的文件系统路径,分析文件使用的缩进,然后打印结果。

如果路径包含子字符串“python_portable”,则搜索将不会深入到该目录。

import os
import re
import codecs

#
# this is the bespoke function I want to port to Scala
#
def generate_all_matching_paths(
    base_dir_abs_path,
    rel_ancestor_dir_list,
    rel_path_matcher
):
    rooted_ancestor_dir_list = [base_dir_abs_path] + rel_ancestor_dir_list
    current_dir_abs_path = os.path.join(*rooted_ancestor_dir_list)

    dir_listing = os.listdir(current_dir_abs_path)
    for fs_item_name in dir_listing:
        fs_item_abs_path = os.path.join(
            current_dir_abs_path,
            fs_item_name
        )
        fs_item_rel_ancestor_list = rel_ancestor_dir_list + [fs_item_name]
        fs_item_rel_path = os.path.join(
            *fs_item_rel_ancestor_list
        )

        result = rel_path_matcher.match(fs_item_rel_path)
        if result.is_match:
            yield fs_item_abs_path
        if result.do_descend and os.path.isdir(fs_item_abs_path):
            child_ancestor_dir_list = rel_ancestor_dir_list + [fs_item_name]
            for r in generate_all_matching_paths(
                base_dir_abs_path,
                child_ancestor_dir_list,
                rel_path_matcher
            ):
                yield r



#
# all following code is only a context giving example of how generate_all_matching_paths might be used
#

class MyMatchResult:

    def __init__(
        self,
        is_match,
        do_descend
    ):
        self.is_match = is_match
        self.do_descend = do_descend

# in Scala this should implement the PathMatcher trait
class MyMatcher:

    def __init__(
        self,
        rel_path_regex,
        abort_dir_descend_regex_list
    ):
        self.rel_path_regex = rel_path_regex
        self.abort_dir_descend_regex_list = abort_dir_descend_regex_list

    def match(self, path):

        rel_path_match = self.rel_path_regex.match(path)
        is_match = rel_path_match is not None

        do_descend = True
        for abort_dir_descend_regex in self.abort_dir_descend_regex_list:
            abort_match = abort_dir_descend_regex.match(path)
            if abort_match:
                do_descend = False
                break

        r = MyMatchResult(is_match, do_descend)
        return r

def leading_whitespace(file_path):
    b_leading_spaces = False
    b_leading_tabs = False

    with codecs.open(file_path, "r", "utf-8") as f:
        for line in f:
            for c in line:
                if c == '\t':
                    b_leading_tabs = True
                elif c == ' ':
                    b_leading_spaces = True
                else:
                    break
            if b_leading_tabs and b_leading_spaces:
                break
    return b_leading_spaces, b_leading_tabs


def print_paths(path_list):
    for path in path_list:
        print(path)


def main():
    leading_spaces_file_path_list = []
    leading_tabs_file_path_list = []
    leading_mixed_file_path_list = []
    leading_none_file_path_list = []

    base_dir_abs_path = r'd:\Projects'

    rel_path_regex = re.compile('.*json$')
    abort_dir_descend_regex_list = [
        re.compile('^.*python_portable.*$')
    ]
    rel_patch_matcher = MyMatcher(rel_path_regex, abort_dir_descend_regex_list)

    ancestor_dir_list = []
    for fs_item_path in generate_all_matching_paths(
        base_dir_abs_path,
        ancestor_dir_list,
        rel_patch_matcher
    ):
        if os.path.isfile(fs_item_path):

            b_leading_spaces, b_leading_tabs = leading_whitespace(fs_item_path)

            if b_leading_spaces and b_leading_tabs:
                leading_mixed_file_path_list.append(fs_item_path)
            elif b_leading_spaces:
                leading_spaces_file_path_list.append(fs_item_path)
            elif b_leading_tabs:
                leading_tabs_file_path_list.append(fs_item_path)
            else:
                leading_none_file_path_list.append(fs_item_path)

    print('space indentation:')
    print_paths(leading_spaces_file_path_list)

    print('tab indentation:')
    print_paths(leading_tabs_file_path_list)

    print('mixed indentation:')
    print_paths(leading_mixed_file_path_list)

    print('no indentation:')
    print_paths(leading_none_file_path_list)

    print('space: {}'.format(len(leading_spaces_file_path_list)))
    print('tab: {}'.format(len(leading_tabs_file_path_list)))
    print('mixed: {}'.format(len(leading_mixed_file_path_list)))
    print('none: {}'.format(len(leading_none_file_path_list)))

if __name__ == '__main__':
    main()

最佳答案

你是对的,你通常会用某种惰性评估来替换 python yield 。下面是一个概念证明,它使用案例类来表示目录,以避免在本示例中执行文件 IO 操作。

case class Directory(val name: String, val files: List[String], val subDirectories: List[Directory])

def descendFilter(directory: Directory): Boolean = directory.name != "tmp"
def matchFilter(path: String): Boolean = path contains "important"

def traverse(directory: Directory, path: String = ""): Stream[String] = {
  val newPath = path + directory.name + "/"
  val files = (directory.files map (newPath + _)).toStream

  val filteredSubdirs = directory.subDirectories filter descendFilter
  val recursedSubdirs = filteredSubdirs map {x => traverse(x, newPath)}
  val combinedSubdirs = recursedSubdirs.fold(Stream.Empty)(_ ++ _)

  (path + directory.name) #:: files ++ combinedSubdirs
}

val directory = Directory("", List(), List(
  Directory("var", List("pid"), List()),
  Directory("opt", List("java"), List()),
  Directory("tmp", List("lots", "of", "temp", "files"), List()),
  Directory("home", List(), List(
    Directory("karl", List("important stuff"), List())
  ))
))

traverse(directory) filter matchFilter foreach println

您基本上可以使用流,就好像它包含整个文件系统一样,但在内部,它只会根据需要获取它们,并以同样快的速度丢弃它们,除非您在其他地方保留对它们的引用。

关于python - 在没有协同例程的情况下编写良好的 Scala(包括使用 Yield 的 Python 示例),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35091623/

相关文章:

scala - 如何处理发出 Future[T] 的源?

ruby - 测试 block 执行的 ruby​​ yield 方法

python - 在文本框中输入时的功能?

python - 在 python 中实现观察者模式的替代方法

scala - 如何将 Java 依赖项添加到 Scala 项目的 sbt 文件

scala - 使用 Scala 隐式覆盖库方法

php - 转储到 JSON 文件在类/函数中无法正常工作

python - 多个 parquet 文件具有 1-2 列的不同数据类型

python-3.x - 使用 next(gen) 和 gen.send(None) 启动 Python 3 生成器有区别吗?

ruby-on-rails - 在 ruby​​ 中,如何在 ruby​​ 中编写 'do' 方法?