Python 的 Java 的 ChainMap?

Python's ChainMap for Java?

我有一个深层嵌套的配置问题。

问题恰好出在机器学习中,最终用户调用交叉验证例程时,可能指定也可能不指定任何各种参数(例如 "randomSeed" = 17)

无论哪种方式,参数都必须首先传递给交叉验证算法,然后再传递给第一个机器学习算法。机器学习算法必须能够在初始用户不知情的情况下设置和传递其他参数。

参数用户链中的大多数消费者都希望 java Map 接口从中进行查找。

出于性能原因,将密钥扁平化到一个库中没有吸引力——CPU 和内存——('root key-name' space)将在不修改的情况下使用数千次,并且每次都需要在传递包之前指定一些额外的参数。

一个不错的类比是 PATH 变量的工作方式,路径中的每个元素都是一个目录(key-namespace)。当对 PATH 变量进行查询时(例如,您在命令行键入 'emacs'),它会在每个目录(键的未命名名称space)中查找该文件名(指定值)按顺序,直到它找到它,或者找不到它。如果它找到了,你就可以执行它找到的可执行文件的具体内容(获取参数集的值)。如果您有来自另一个的 PATH 变量,则可以在将该 PATH 变量设置传递给新的最终用户时在其前面附加一个新目录(匿名键-space),而无需修改以前的目录(偏好)。

鉴于配置参数上的名称-space 实际上是扁平的,解决方案类似于 Python 的 ChainMap would be perfect (eg example usage) 但我在 [=31 中找不到等效解决方案=]?

开箱即用我不知道等效项。 Guava 也不提供 Maps.chain() 方法,但也许应该提供。

对于正常用例,我会简单地构造一个新的 Map;使用番石榴,您可以简洁地做到这一点,例如:

// Entries in map2 overwrite map1; reverse the insertion order if needed
ImmutableMap.builder().putAll(map1).putAll(map2).build();

您可以定义自己的 ChainMap<K, V> 实现,通过扩展 AbstractMap 来相对轻松地存储 List<Map<? extends K, ? extends V>>。然而,这会引入 O(k) 开销(其中 k 是链中映射的数量)。如果您的地图太大以至于无法复制,或者您打算非常频繁地构建这些链接映射,那么这将是值得的,否则我不建议这样做。

对于像合并属性这样的用例,我只会构建一个新地图并完成它。

由于似乎没有现成的东西(感谢 lopisan 和 dimo414 的 tips/pointers),我做了第一个破解实现,至少可以满足我的即时需求。我会推迟几天将其标记为答案,以防有人知道图书馆级版本。

我已经包含了很多示例用法调用。它们可以转换为单元测试。有些地方它可以更有效率。

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;


/** 
 * Not thread safe. To make it thread safe, you'd also have to ensure the underlying collections are thread safe.
 * Expected use case is  indexing Strings
 */
public class ChainMap<K, V>  implements Map<K, V>, Cloneable{ 
    ArrayList<  Map<K, V> > scopes = new ArrayList<>(); 

    @Override
    public V get(Object key) {

        for( Map<K, V> m : Lists.reverse( scopes ))
            if( m.containsKey( key)) return  m.get(key);

        //no one has it..
        return null; 
    }


    public  void pushScope( Map< K, V>  scope){
        scopes.add( scope); 
    }

    public  Map<K, V>  popScope( ) {
        if( scopes.size() == 0) throw new RuntimeException("must have at least one underlying map in the Chain to do a pop"); 

        return scopes.remove( scopes.size() -1); 
    }


    /** warning, this risks being expensive, as the semantics are interpreted as the count of distinct keys. 
     * you may want to cache this value*/ 
    public int size() {        return keySet().size();    }


    public boolean isEmpty() {
        for(  Map<K, V> m : scopes  )         //no reverese iteration  needed
            if( !m.isEmpty()) return false; 
        return true; 
    }


    public boolean containsKey(Object key) { 
        for(  Map<K, V> m : scopes  )       //no reverese iteration  needed
            if( m.containsKey( key)) return true;   
        return false; 
    }


    public boolean containsValue(Object value) {
        for(  Entry<K, V> e : entrySet())
            if( (value == e.getValue() || (value != null && value.equals(e.getValue()))) == true) 
                return true; 

        return false; 
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Map) {
            return entrySet().equals(((Map<?,?>)obj).entrySet());
        }
        return false;
    }

    @Override
    public int hashCode() {
        return entrySet().hashCode();
    }


    public V put(K key, V value) {
        if( scopes.size() == 0) throw new RuntimeException("must have at least one underlying map in the Chain to do a put"); 

        return scopes.get( scopes.size()-1).put( key, value); 
    }


    public V remove(Object key) {
        return scopes.get( scopes.size()-1).remove( key); 
    }


    public void putAll(Map<? extends K, ? extends V> m) {
        if( scopes.size() == 0) throw new RuntimeException("must have at least one underlying map in the Chain to do a put"); 
        scopes.get( scopes.size()-1).putAll( m); 
    }


    /** clears only the last, by default */
    public void clear() { 
        scopes.get( scopes.size()-1).clear();   
   }


    /** builds the result as a view on the underlying keySet */
    public Set<K> keySet() {
        int numMaps = scopes.size(); 
        if( numMaps == 0) return Collections.emptySet(); 
        else if( numMaps == 1) return scopes.get(0).keySet(); 
        else{ 

            Set<K> result = Sets.union( scopes.get(numMaps-1).keySet(), scopes.get(numMaps-2).keySet()); 

            for (int i = scopes.size()-3; i >=0 ; i--) 
                result = Sets.union( result, scopes.get( i).keySet());

            return result; 
        }
    }

    public Collection<V> values() {
        return this.entrySet().stream().map( e -> e.getValue()).collect(Collectors.toList()); 
    }

    /** builds the result as a view on the underlying entrySets */
    public Set<Map.Entry<K, V>> entrySet() {
        int numMaps = scopes.size(); 
        if( numMaps == 0) return new HashMap<K, V>().entrySet();
        else if( numMaps == 1) return scopes.get(0).entrySet(); 
        else{ 

            Set<K> keySet = this.keySet(); 
            Map<K, V> m = Maps.asMap( keySet,    key -> this.get(key)); 
            return m.entrySet(); //return Maps.asMap( keySet,    key -> this.get(key)).keySet();  
        }
    }


    @SuppressWarnings("unchecked")
    public Object clone(){
        try {
            ChainMap< K, V> cm  = (ChainMap< K, V>) super.clone();
            cm.scopes = (ArrayList<  Map<K, V> > ) this.scopes.clone();
            return cm; 
        } catch (CloneNotSupportedException e) {
            throw new Error( e ); 
        }
    }


    public ChainMap<K, V> copy(){ 
        @SuppressWarnings("unchecked")
        ChainMap<K, V> c  = (ChainMap<K, V>) clone();
        return c; 
    }








    public static void 
    examples1
    ( )
    {
        ChainMap<String, Object> cm1 = new ChainMap<>();  
        HashMap<String, Object> a = new HashMap<>();
        a.put( "a", "A"); 
        a.put( "b", "B"); 
        a.put( "c", "C"); 
        a.put( "m", "M"); 
        a.put( "a'sMap", "asValue");  //<-- tracer entry

        HashMap<String, Object> b = new HashMap<>();
        b.put( "c", "CCC"); 
        b.put( "b'sMap", "bsValue");   //<-- tracer entry

        HashMap<String, Object> c = new HashMap<>();
        c.put( "a", "AAA"); 
        c.put( "b",  1); 
        c.put( "z", "ZZZ"); 
        c.put( "c'sMap", "csMapValue");   //<-- tracer entry

        cm1.pushScope( a);
        cm1.pushScope( b);
        cm1.pushScope( c);
        PrintStream o = System.out; 

        o.println( cm1.get( "z")); //prints "ZZZ"
        o.println( cm1.get( "b")); //prints 1

        cm1.put( "z", 5);
        o.println( cm1.get( "z")); //prints 5

        ChainMap<String, Object> cm2 = cm1.copy();

        HashMap<String, Object> d = new HashMap<>();
        d.put( "a", 999);
        d.put( "w", "WWWWWWW");
        d.put( "x", "XXXXXXX");
        d.put( "t", "TTTTTTT");
        d.put( "d'sMap", "dsMapValue");  //<-- tracer entry
        cm2.pushScope(d); 

        ChainMap<String, Object> cm3 = cm2.copy();  

        o.println( cm2.get( "a")); //prints "999"
        o.println( cm1.get( "a")); //prints "AAA"

        cm2.popScope(); 
        cm2.popScope();
        o.println( cm2.get("a"));//prints "A"


        o.println( cm3.keySet().size()); 


        o.println( "__________"); 
        //show how can iterate keys-value pairs
        for( Entry<String, Object>  e: cm3.entrySet()) 
            o.println( e.getKey() + ":" + e.getValue()); 

        o.println( "__________"); 

        o.println( cm3.keySet().contains( "w")); //prints true


        o.println( cm3.containsKey( "f")); //prints false 
        o.println( cm3.containsKey( "a")); //prints true
        o.println( cm3.containsKey( "w")); //prints true


        cm3.popScope(); 
        o.println( cm3.containsKey( "w")); //prints false

    }


    public static void 
    examples2
    ( )
    {
        ChainMap<String, Object> cm1 = new ChainMap<>();  
        HashMap<String, Object> a = new HashMap<>();
        a.put( "a", "A"); 
        a.put( "a'sMap", "asValue"); 

        HashMap<String, Object> b = new HashMap<>();
        b.put( "b", "BBB"); 
        b.put( "b'sMap", "bsValue"); 

        HashMap<String, Object> c = new HashMap<>();
        c.put( "c", "CCC"); 
        c.put( "c'sMap", "csMapValue"); 

        HashMap<String, Object> d = new HashMap<>();
        d.put( "d", "DDD"); 
        d.put( "d'sMap", "dsMapValue"); 

        cm1.pushScope( a);
        cm1.pushScope( b);
        cm1.pushScope( c);
        PrintStream o = System.out; 

        // we can make a chainMap part of another 
        ChainMap<String, Object> cmMeta = new ChainMap<>(); 
        cmMeta.pushScope( cm1);
        cmMeta.pushScope( d);


        o.println( "__________"); 
        for( Entry<String, Object>  e: cmMeta.entrySet()) 
            o.println( e.getKey() + ":" + e.getValue()); 
        o.println( "__________"); 

        /*Gives: 
            __________
            d'sMap:dsMapValue
            d:DDD
            c:CCC
            c'sMap:csMapValue
            b:BBB
            b'sMap:bsValue
            a:A
            a'sMap:asValue
            __________
         */

    }

    public static void   main( String[] args ) {   examples1();  examples2();     }

}

因为它模拟了 python ChainMap API,请注意 aChainMapInstance.remove(someKey) 并不意味着密钥不会仍然在那里。仅当范围堆栈上的顶部映射包含键时,该删除调用才会起作用。

(已更新以模拟 dimo414 对 containsValue()hashcode()equals() 的实施。)


顺便说一句,在尝试构建它时,我注意到有两个映射与番石榴的隐式融合的单线版本(如果这就是您所需要的)。一个示例用例:两个地图可能都有数百万个键,您的用户甚至可能根本懒得去查询它。但是,与上面的 ChainMap 不同的是,修改会给您一个 UnsupportedOperationException。这个应该通过嵌套来工作,比如:composedMapView( map1, composedMapView( map2, map3))。

 public static <K, V> 
    Map<K, V> composedMapView( Map<K, V> look1st, Map<K, V> look2nd){
        return  Maps.asMap( Sets.union( look1st.keySet(), look2nd.keySet()), 
                    k ->  look1st.containsKey(k) ? look1st.get(k) : look2nd.get(k));  

    }

周末我继续创建了 ChainMap implementation as well; thanks to Java 8 it's a surprisingly small class. My implementation is slightly different than yours; it doesn't attempt to mirror Python's behavior and instead follows the Map 接口规范。特别是:

  • 查找顺序为插入顺序;传递给构造函数的第一个映射优先于以下映射。
  • .containsValue() 与早期地图掩盖的值不匹配。
  • .put() returns 链映射的先前值,即使该值在以后的映射中也是如此。
  • .remove() 从所有映射中删除键,而不仅仅是第一个映射或可见条目。来自 Javadoc:“ 一旦调用 returns.,映射将不包含指定键的映射”
  • 同样.clear()清除所有地图,而不仅仅是顶部地图。
  • 在其条目集的基础上实现.equals().hashCode(),因此它与其他Map实现相同。

我也没有实现 push/pop 行为,因为它感觉像是一种反模式; ChainMap 已经是一系列地图的 O(1) 视图,您可以根据需要使用您想要的地图简单地构建额外的 ChainMaps。

显然,如果您的实施适用于您的用例,那就太好了。但它在几个地方违反了 Map 合同;我强烈建议删除 implements Map<K, V> 并让它成为一个独立的 class.

class 的许多方法都是很好的单行代码,例如:

@Override
public int size() {
  return keySet().size();
}

@Override
public boolean isEmpty() {
  return !chain.stream().filter(map -> !map.isEmpty()).findFirst().isPresent();
}

@Override
public boolean containsKey(Object key) {
  return chain.stream().filter(map -> map.containsKey(key)).findFirst().isPresent();
}

@Override
public boolean containsValue(Object value) {
  return entrySet().stream()
    .filter(e -> value == e.getValue() || (value != null && value.equals(e.getValue())))
    .findFirst().isPresent();
}

@Override
public V get(Object key) {
  return chain.stream().filter(map -> map.containsKey(key))
    .findFirst().map(map -> map.get(key)).orElse(null);
}

我也写了一些 tests 来验证 class 的行为。欢迎补充测试用例。


我还扩展了您使用 Maps.asMap() 创建地图集合的不可变视图的想法;如果你不需要突变,这会很好地工作。 (作为 ,您必须使用 .reduce() 的三参数形式来让泛型起作用)。

public static <K, V> Map<K, V> immutableChainView(
    Iterable<? extends Map<? extends K, ? extends V>> maps) {
  return StreamSupport.stream(maps.spliterator(), false).reduce(
    (Map<K,V>)ImmutableMap.<K,V>of(),
    (a, b) -> Maps.asMap(Sets.union(a.keySet(), b.keySet()),
                         k -> a.containsKey(k) ? a.get(k) : b.get(k)),
    (a, b) -> Maps.asMap(Sets.union(a.keySet(), b.keySet()),
                         k -> a.containsKey(k) ? a.get(k) : b.get(k)));
    }