提交 a19013dd authored 作者: Arnaud Bergeron's avatar Arnaud Bergeron

Flake8 for compile/function_module.py

上级 e710ee08
"""Driver of graph construction, optimization, and linking. """Driver of graph construction, optimization, and linking.
""" """
from __future__ import print_function from __future__ import print_function
...@@ -35,7 +34,7 @@ class UnusedInputError(Exception): ...@@ -35,7 +34,7 @@ class UnusedInputError(Exception):
def alias_root(v): def alias_root(v):
"""Return the variable to which v is aliased by view_maps and destroy_maps""" "Return the variable to which v is aliased by view_maps and destroy_maps"
if v.owner is None: if v.owner is None:
return v return v
vmap = getattr(v.owner.op, 'view_map', {}) vmap = getattr(v.owner.op, 'view_map', {})
...@@ -54,7 +53,8 @@ def alias_root(v): ...@@ -54,7 +53,8 @@ def alias_root(v):
def view_tree_set(v, treeset): def view_tree_set(v, treeset):
"""Add to `treeset` all variables that are views of v, given that v is not a view""" """Add to `treeset` all variables that are views of v, given that v is
not a view"""
treeset.add(v) treeset.add(v)
for cl, v_input_pos_to_cl in v.clients: for cl, v_input_pos_to_cl in v.clients:
if cl == 'output': if cl == 'output':
...@@ -69,11 +69,13 @@ def view_tree_set(v, treeset): ...@@ -69,11 +69,13 @@ def view_tree_set(v, treeset):
def infer_reuse_pattern(fgraph, outputs_to_disown): def infer_reuse_pattern(fgraph, outputs_to_disown):
""" """
Given an fgraph and a list of variables, returns the list or set of all variables which may Given an fgraph and a list of variables, returns the list or set
share the same underlying data storage as any of the specified variables. Used internally of all variables which may share the same underlying data storage
by function, FunctionMaker. as any of the specified variables. Used internally by function,
FunctionMaker.
This list (or set) is also refered to as no_recycling sometimes, especially by linker code. This list (or set) is also refered to as no_recycling sometimes,
especially by linker code.
""" """
rval = set() rval = set()
for o in outputs_to_disown: for o in outputs_to_disown:
...@@ -103,10 +105,10 @@ def fgraph_updated_vars(fgraph, expanded_inputs): ...@@ -103,10 +105,10 @@ def fgraph_updated_vars(fgraph, expanded_inputs):
class Supervisor: class Supervisor:
""" """
Listener for FunctionGraph events which makes sure that no operation overwrites the Listener for FunctionGraph events which makes sure that no
contents of protected Variables. The outputs of the FunctionGraph are protected by default. operation overwrites the contents of protected Variables. The
outputs of the FunctionGraph are protected by default.
""" """
def __init__(self, protected): def __init__(self, protected):
self.protected = list(protected) self.protected = list(protected)
...@@ -176,33 +178,38 @@ class AliasedMemoryError(Exception): ...@@ -176,33 +178,38 @@ class AliasedMemoryError(Exception):
# Function # Function
### ###
# unique id object used as a placeholder for duplicate entries
DUPLICATE = ['DUPLICATE'] # unique id object used as a placeholder for duplicate entries DUPLICATE = ['DUPLICATE']
class Function(object): class Function(object):
""" """
Type of the functions returned by theano.function or theano.FunctionMaker.create. Type of the functions returned by theano.function or
theano.FunctionMaker.create.
`Function` is the callable object that does computation. It has the storage of inputs and
outputs, performs the packing and unpacking of inputs and return values. It implements the
square-bracket indexing so that you can look up the value of a symbolic node.
Functions are copyable via {{{fn.copy()}}} and {{{copy.copy(fn)}}}.
When a function is copied, this instance is duplicated. Contrast with self.maker
(instance of `FunctionMaker`) that is shared between copies.
The meaning of copying a function is that the containers and their current values will all be duplicated.
This requires that mutable inputs be copied, whereas immutable inputs may be shared between copies.
`Function` is the callable object that does computation. It has
the storage of inputs and outputs, performs the packing and
unpacking of inputs and return values. It implements the
square-bracket indexing so that you can look up the value of a
symbolic node.
Functions are copyable via {{{fn.copy()}}} and
{{{copy.copy(fn)}}}. When a function is copied, this instance is
duplicated. Contrast with self.maker (instance of
`FunctionMaker`) that is shared between copies. The meaning of
copying a function is that the containers and their current values
will all be duplicated. This requires that mutable inputs be
copied, whereas immutable inputs may be shared between copies.
A Function instance is hashable, on the basis of its memory address (its id). A Function instance is hashable, on the basis of its memory
address (its id).
A Function instance is only equal to itself. A Function instance is only equal to itself.
A Function instance may be serialized using the `pickle` or `cPickle` modules. A Function instance may be serialized using the `pickle` or
This will save all default inputs, the graph, and *** to the pickle file (WRITEME). `cPickle` modules. This will save all default inputs, the graph,
and *** to the pickle file (WRITEME).
A Function instance have a ``trust_input`` field that default to A Function instance have a ``trust_input`` field that default to
False. When True, we don't do extra check of the input to give False. When True, we don't do extra check of the input to give
...@@ -210,7 +217,6 @@ class Function(object): ...@@ -210,7 +217,6 @@ class Function(object):
the good results if you pass a python or numpy scalar instead of a the good results if you pass a python or numpy scalar instead of a
numpy tensor. C code should raise an error if you pass an object numpy tensor. C code should raise an error if you pass an object
of the wrong type. of the wrong type.
""" """
pickle_aliased_memory_strategy = 'warn' pickle_aliased_memory_strategy = 'warn'
...@@ -218,12 +224,11 @@ class Function(object): ...@@ -218,12 +224,11 @@ class Function(object):
Meaningful settings are: 'ignore', 'warn', 'raise' Meaningful settings are: 'ignore', 'warn', 'raise'
If the value is 'warn', then a message will be printed to stderr if aliased storage is If the value is 'warn', then a message will be printed to stderr
dectected during pickle.dump. if aliased storage is dectected during pickle.dump.
If the value is 'raise', then an AliasedMemoryError will be raised if aliased storage is
detected during pickle.dump.
If the value is 'raise', then an AliasedMemoryError will be raised
if aliased storage is detected during pickle.dump.
""" """
input_storage = None input_storage = None
...@@ -233,24 +238,28 @@ class Function(object): ...@@ -233,24 +238,28 @@ class Function(object):
"""list of Container instances""" """list of Container instances"""
indices = None indices = None
"""list of (SymbolicInput|SymbolicInputKit, indices, [SymbolicInput,...]), one tuple for """list of (SymbolicInput|SymbolicInputKit, indices,
each input [SymbolicInput,...]), one tuple for each input
The first tuple element is the SymbolicInput object for the corresponding function input. The first tuple element is the SymbolicInput object for the
corresponding function input.
The second and third tuple elements are used only by Kits, which are deprecated. The second and third tuple elements are used only by Kits, which
are deprecated.
""" """
defaults = None defaults = None
""" list of 3-tuples, one 3-tuple for each input. """ list of 3-tuples, one 3-tuple for each input.
Tuple element 0: Bool: Is this input required at each function call? Tuple element 0: Bool: Is this input required at each function call?
Tuple element 1: Bool: Should this inputs value be reverted after each call? Tuple element 1: Bool: Should this inputs value be reverted after
each call?
Tuple element 2: Any: The value associated with this input. Tuple element 2: Any: The value associated with this input.
""" """
unpack_single = None unpack_single = None
"""Bool: for outputs lists of length 1, should the 0'th element be returned directly?""" """Bool: for outputs lists of length 1, should the 0'th element be
returned directly?"""
return_none = None return_none = None
"""Bool: whether the function should return None or not""" """Bool: whether the function should return None or not"""
...@@ -259,8 +268,8 @@ class Function(object): ...@@ -259,8 +268,8 @@ class Function(object):
"""FunctionMaker instance""" """FunctionMaker instance"""
fn = None fn = None
"""a function that evaluates the graph. Typically a linker's make_thunk method created this """a function that evaluates the graph. Typically a linker's
function.""" make_thunk method created this function."""
finder = None finder = None
"""Dictionary mapping several kinds of things to containers. """Dictionary mapping several kinds of things to containers.
...@@ -273,7 +282,8 @@ class Function(object): ...@@ -273,7 +282,8 @@ class Function(object):
- the name of the input - the name of the input
All entries map to the container or to DUPLICATE if an ambiguity is detected All entries map to the container or to DUPLICATE if an ambiguity
is detected
""" """
inv_finder = None inv_finder = None
...@@ -312,20 +322,22 @@ class Function(object): ...@@ -312,20 +322,22 @@ class Function(object):
input.distribute(value, indices, cs) input.distribute(value, indices, cs)
for c in cs: for c in cs:
c.provided += 1 c.provided += 1
# def assign(c, v):
#c.data = v
# Store the list of names of named inputs. # Store the list of names of named inputs.
named_inputs = [] named_inputs = []
# Count the number of un-named inputs. # Count the number of un-named inputs.
n_unnamed_inputs = 0 n_unnamed_inputs = 0
#setters = []
# Initialize the storage # Initialize the storage
# this loop works by modifying the elements (as variable c) of self.input_storage inplace. # this loop works by modifying the elements (as variable c) of
for i, ((input, indices, sinputs), (required, refeed, value)) in enumerate(zip(self.indices, defaults)): # self.input_storage inplace.
if indices is None: # this is true iff input is not a SymbolicInputKit for i, ((input, indices, sinputs), (required, refeed, value)) in \
c = containers[0] #containers is being used as a stack. Here we pop off the next one. enumerate(zip(self.indices, defaults)):
# this is true iff input is not a SymbolicInputKit
if indices is None:
# containers is being used as a stack. Here we pop off
# the next one.
c = containers[0]
c.strict = getattr(input, 'strict', False) c.strict = getattr(input, 'strict', False)
c.allow_downcast = getattr(input, 'allow_downcast', None) c.allow_downcast = getattr(input, 'allow_downcast', None)
...@@ -342,7 +354,9 @@ class Function(object): ...@@ -342,7 +354,9 @@ class Function(object):
c.value = value c.value = value
c.required = required c.required = required
c.implicit = input.implicit c.implicit = input.implicit
c.provided = 0 # this is a count of how many times the input has been provided (reinitialized to 0 on __call__) # this is a count of how many times the input has been
# provided (reinitialized to 0 on __call__)
c.provided = 0
finder[i] = c finder[i] = c
finder[input.variable] = c finder[input.variable] = c
if input.name not in finder: if input.name not in finder:
...@@ -353,17 +367,14 @@ class Function(object): ...@@ -353,17 +367,14 @@ class Function(object):
n_unnamed_inputs += 1 n_unnamed_inputs += 1
else: else:
named_inputs.append(input.name) named_inputs.append(input.name)
# backport
#finder[input.name] = c if input.name not in finder else DUPLICATE
# inv_finder maps the container to the input (useful for one error message)
inv_finder[c] = input inv_finder[c] = input
#setters.append(partial(assign, c))
containers[:1] = [] containers[:1] = []
else: else:
# TODO The following code may need to do something to handle # TODO The following code may need to do something to handle
# implicit inputs. # implicit inputs.
# The input is a SymbolicInputKit, so we take as many containers as the Kit provides inputs # The input is a SymbolicInputKit, so we take as many
# containers as the Kit provides inputs
cs = containers[:len(indices)] cs = containers[:len(indices)]
# distribute does the initialization of the containers # distribute does the initialization of the containers
input.distribute(value, indices, cs) input.distribute(value, indices, cs)
...@@ -377,12 +388,11 @@ class Function(object): ...@@ -377,12 +388,11 @@ class Function(object):
finder[input.name] = f finder[input.name] = f
else: else:
finder[input.name] = DUPLICATE finder[input.name] = DUPLICATE
# backport # For each input in the kit and its corresponding
#finder[input.name] = f if input.name not in finder else DUPLICATE # container, we put an entry in finder. This allows
# setters.append(f) # the user to micro-manage elements of the kit if need
# For each input in the kit and its corresponding container, we put an entry in finder. # be. All containers inherit the required field and
# This allows the user to micro-manage elements of the kit if need be. # have their own "provided" counter
# All containers inherit the required field and have their own "provided" counter
for c, sin in zip(cs, sinputs): for c, sin in zip(cs, sinputs):
finder[sin.variable] = c finder[sin.variable] = c
finder[sin.name] = c finder[sin.name] = c
...@@ -390,8 +400,6 @@ class Function(object): ...@@ -390,8 +400,6 @@ class Function(object):
finder[sin.name] = c finder[sin.name] = c
else: else:
finder[sin.name] = DUPLICATE finder[sin.name] = DUPLICATE
# backport
#finder[sin.name] = c if sin.name not in finder else DUPLICATE
inv_finder[c] = input inv_finder[c] = input
c.required = required c.required = required
c.provided = 0 c.provided = 0
...@@ -410,12 +418,14 @@ class Function(object): ...@@ -410,12 +418,14 @@ class Function(object):
except KeyError: except KeyError:
raise TypeError("Unknown input or state: %s" % str(item)) raise TypeError("Unknown input or state: %s" % str(item))
if s is DUPLICATE: if s is DUPLICATE:
raise TypeError("Ambiguous name: %s - please check the names "\ raise TypeError("Ambiguous name: %s - please check the "
"of the inputs of your function for duplicates." % str(item)) "names of the inputs of your function "
"for duplicates." % str(item))
if isinstance(s, gof.Container): if isinstance(s, gof.Container):
return s.value return s.value
else: else:
raise NotImplementedError raise NotImplementedError
def __setitem__(self, item, value): def __setitem__(self, item, value):
try: try:
s = finder[item] s = finder[item]
...@@ -425,13 +435,15 @@ class Function(object): ...@@ -425,13 +435,15 @@ class Function(object):
raise TypeError("Unknown input or state: %s. %s" % raise TypeError("Unknown input or state: %s. %s" %
(str(item), msg)) (str(item), msg))
if s is DUPLICATE: if s is DUPLICATE:
raise TypeError("Ambiguous name: %s - please check the names "\ raise TypeError("Ambiguous name: %s - please check the "
"of the inputs of your function for duplicates." % str(item)) "names of the inputs of your function "
"for duplicates." % str(item))
if isinstance(s, gof.Container): if isinstance(s, gof.Container):
s.value = value s.value = value
s.provided += 1 s.provided += 1
else: else:
s(value) s(value)
def __contains__(self, item): def __contains__(self, item):
return finder.__contains__(item) return finder.__contains__(item)
...@@ -441,6 +453,7 @@ class Function(object): ...@@ -441,6 +453,7 @@ class Function(object):
class ContainerAttribute(object): class ContainerAttribute(object):
def __getitem__(self, item): def __getitem__(self, item):
return finder[item] return finder[item]
def __contains__(self, item): def __contains__(self, item):
return finder.__contains__(item) return finder.__contains__(item)
# You cannot set the container # You cannot set the container
...@@ -513,20 +526,17 @@ class Function(object): ...@@ -513,20 +526,17 @@ class Function(object):
s.storage[0] = arg s.storage[0] = arg
else: else:
try: try:
s.storage[0] = s.type.filter(arg, strict=s.strict, s.storage[0] = s.type.filter(
arg, strict=s.strict,
allow_downcast=s.allow_downcast) allow_downcast=s.allow_downcast)
except Exception as e: except Exception as e:
function_name = "theano function" function_name = "theano function"
if self.name: if self.name:
function_name += ' with name "' + self.name + '" ' function_name += ' with name "' + self.name + '" '
# end if e.args = ("Bad input argument to " + function_name +
e.args = tuple(["Bad input argument to " + function_name + " at index %d(0-based)" % i,) + e.args
" at index %d(0-based)" % i] +
list(e.args))
raise raise
# end except
# end if
s.provided += 1 s.provided += 1
i += 1 i += 1
...@@ -535,9 +545,8 @@ class Function(object): ...@@ -535,9 +545,8 @@ class Function(object):
for k, arg in kwargs.iteritems(): for k, arg in kwargs.iteritems():
self[k] = arg self[k] = arg
if not self.trust_input and ( if (not self.trust_input and
not hasattr(self, '_check_for_aliased_inputs') or getattr(self, '_check_for_aliased_inputs', True)):
self._check_for_aliased_inputs):
# Collect aliased inputs among the storage space # Collect aliased inputs among the storage space
args_share_memory = [] args_share_memory = []
for i in xrange(len(self.input_storage)): for i in xrange(len(self.input_storage)):
...@@ -566,10 +575,6 @@ class Function(object): ...@@ -566,10 +575,6 @@ class Function(object):
# Check for groups of more than one argument that share memory # Check for groups of more than one argument that share memory
for group in args_share_memory: for group in args_share_memory:
if len(group) > 1: if len(group) > 1:
# see if any of these arguments are mutable
mutable = numpy.any([(self.maker.inputs[idx].mutable or
self.maker.inputs[idx].borrow)
for idx in group])
# copy all but the first # copy all but the first
for idx in group[1:]: for idx in group[1:]:
self.input_storage[i].storage[0] = copy.copy( self.input_storage[i].storage[0] = copy.copy(
...@@ -696,13 +701,15 @@ class Function(object): ...@@ -696,13 +701,15 @@ class Function(object):
container = property( container = property(
lambda self: self._container, lambda self: self._container,
None, # this property itself is not settable None, # this property itself is not settable
doc="""dictionary-like access to the containers associated with Variables""") doc=("dictionary-like access to the containers associated with "
"Variables"))
def free(self): def free(self):
""" """
When allow_gc = False, clear the Variables in storage_map When allow_gc = False, clear the Variables in storage_map
""" """
# 1.no allow_gc return False 2.has allow_gc, if allow_gc is False, return True # 1.no allow_gc return False
# 2.has allow_gc, if allow_gc is False, return True
if not getattr(self.fn, 'allow_gc', True): if not getattr(self.fn, 'allow_gc', True):
for key in self.fn.storage_map.keys(): for key in self.fn.storage_map.keys():
if not isinstance(key, theano.gof.Constant): if not isinstance(key, theano.gof.Constant):
...@@ -719,7 +726,8 @@ def _pickle_Function(f): ...@@ -719,7 +726,8 @@ def _pickle_Function(f):
ins = list(f.input_storage) ins = list(f.input_storage)
input_storage = [] input_storage = []
for (input, indices, inputs), (required, refeed, default) in zip(f.indices, f.defaults): for (input, indices, inputs), (required, refeed, default) in \
zip(f.indices, f.defaults):
if isinstance(input, SymbolicInputKit): if isinstance(input, SymbolicInputKit):
li = len(indices) li = len(indices)
if not default: if not default:
...@@ -734,18 +742,21 @@ def _pickle_Function(f): ...@@ -734,18 +742,21 @@ def _pickle_Function(f):
inputs_data = [x.data for x in f.input_storage] inputs_data = [x.data for x in f.input_storage]
# HACK to detect aliased storage. # HACK to detect aliased storage.
# This is here because aliased relationships are not [currently] preserved across the pickle operation # This is here because aliased relationships are not [currently]
# preserved across the pickle operation
if not (f.pickle_aliased_memory_strategy == 'ignore'): if not (f.pickle_aliased_memory_strategy == 'ignore'):
all_data = input_storage + inputs_data # addition here means list append all_data = input_storage + inputs_data
for i, d_i in enumerate(all_data): for i, d_i in enumerate(all_data):
for j, d_j in enumerate(all_data): for j, d_j in enumerate(all_data):
if (i < j) and isinstance(d_i, numpy.ndarray) and isinstance(d_j, numpy.ndarray): if ((i < j) and isinstance(d_i, numpy.ndarray) and
isinstance(d_j, numpy.ndarray)):
if numpy.may_share_memory(d_i, d_j): if numpy.may_share_memory(d_i, d_j):
if f.pickle_aliased_memory_strategy == 'warn': if f.pickle_aliased_memory_strategy == 'warn':
_logger.warning(('aliased relationship between' _logger.warning('aliased relationship between '
' Function arguments %s, %s' 'Function arguments %s, %s '
' will not be preserved by un-pickling' 'will not be preserved by '
' operation') % (str(d_i), str(d_j))) 'un-pickling operation' %
(str(d_i), str(d_j)))
else: else:
raise AliasedMemoryError(d_i, d_j) raise AliasedMemoryError(d_i, d_j)
rval = (_constructor_Function, (f.maker, input_storage, inputs_data)) rval = (_constructor_Function, (f.maker, input_storage, inputs_data))
...@@ -774,20 +785,25 @@ def insert_deepcopy(fgraph, wrapped_inputs, wrapped_outputs): ...@@ -774,20 +785,25 @@ def insert_deepcopy(fgraph, wrapped_inputs, wrapped_outputs):
""" """
Insert deepcopy in the fgraph to break aliasing of outputs Insert deepcopy in the fgraph to break aliasing of outputs
""" """
# This loop was inserted to remove aliasing between outputs when they all # This loop was inserted to remove aliasing between outputs when
# evaluete to the same value. Originally it was OK for outputs to be aliased, # they all evaluete to the same value. Originally it was OK for
# but some of the outputs can be shared variables, and is not good for shared # outputs to be aliased, but some of the outputs can be shared
# variables to be aliased. It might be possible to optimize this by making sure # variables, and is not good for shared variables to be
# aliased. It might be possible to optimize this by making sure
# there is no aliasing only between shared variables. # there is no aliasing only between shared variables.
# If some outputs are constant, we add deep copy to respect the memory contract # If some outputs are constant, we add deep copy to respect the
# memory contract
# We don't insert deep copy when the output.borrow is True for all conserned outputs. # We don't insert deep copy when the output.borrow is True for all
# conserned outputs.
assert len(wrapped_inputs) == len(fgraph.inputs) assert len(wrapped_inputs) == len(fgraph.inputs)
assert len(wrapped_outputs) == len(fgraph.outputs) assert len(wrapped_outputs) == len(fgraph.outputs)
reason = "insert_deepcopy" reason = "insert_deepcopy"
updated_fgraph_inputs = [fgraph_i for i, fgraph_i in zip(wrapped_inputs, fgraph.inputs) if getattr(i, 'update', False)] updated_fgraph_inputs = [fgraph_i for i, fgraph_i in
zip(wrapped_inputs, fgraph.inputs)
if getattr(i, 'update', False)]
# We can't use fgraph.inputs as this don't include Constant Value. # We can't use fgraph.inputs as this don't include Constant Value.
all_graph_inputs = gof.graph.inputs(fgraph.outputs) all_graph_inputs = gof.graph.inputs(fgraph.outputs)
...@@ -802,10 +818,12 @@ def insert_deepcopy(fgraph, wrapped_inputs, wrapped_outputs): ...@@ -802,10 +818,12 @@ def insert_deepcopy(fgraph, wrapped_inputs, wrapped_outputs):
# and not(wrapped_outputs[i].borrow and wrapped_outputs[j].borrow): # and not(wrapped_outputs[i].borrow and wrapped_outputs[j].borrow):
if fgraph.outputs[j] in views_of_output_i: if fgraph.outputs[j] in views_of_output_i:
if wrapped_outputs[i].borrow and wrapped_outputs[j].borrow: if wrapped_outputs[i].borrow and wrapped_outputs[j].borrow:
fgraph.change_input('output', i, view_op(fgraph.outputs[i]), fgraph.change_input('output', i,
view_op(fgraph.outputs[i]),
reason=reason) reason=reason)
else: else:
fgraph.change_input('output', i, deep_copy_op(fgraph.outputs[i]), fgraph.change_input('output', i,
deep_copy_op(fgraph.outputs[i]),
reason=reason) reason=reason)
copied = True copied = True
break break
...@@ -813,31 +831,40 @@ def insert_deepcopy(fgraph, wrapped_inputs, wrapped_outputs): ...@@ -813,31 +831,40 @@ def insert_deepcopy(fgraph, wrapped_inputs, wrapped_outputs):
if not copied: if not copied:
for input_j in all_graph_inputs: for input_j in all_graph_inputs:
# do not allow outputs to be aliased to an inputs (j), unless # do not allow outputs to be aliased to an inputs (j), unless
# a) that j'th input has been 'destroyed' by e.g. in-place computations # a) that j'th input has been 'destroyed' by
# b) that j'th input is a shared variable that is also being updated # e.g. in-place computations
# b) that j'th input is a shared variable that is also
# being updated
if (hasattr(fgraph, 'get_destroyers_of') and if (hasattr(fgraph, 'get_destroyers_of') and
fgraph.get_destroyers_of(input_j)): fgraph.get_destroyers_of(input_j)):
continue continue
if input_j in updated_fgraph_inputs: if input_j in updated_fgraph_inputs:
continue continue
if input_j in views_of_output_i: if input_j in views_of_output_i:
# We don't put deep_copy_op if the input and the output have borrow==True # We don't put deep_copy_op if the input and the
# output have borrow==True
if input_j in fgraph.inputs: if input_j in fgraph.inputs:
j = fgraph.inputs.index(input_j) j = fgraph.inputs.index(input_j)
if wrapped_outputs[i].borrow and wrapped_inputs[j].borrow: if (wrapped_outputs[i].borrow and
fgraph.change_input('output', i, view_op(fgraph.outputs[i]), wrapped_inputs[j].borrow):
fgraph.change_input('output', i,
view_op(fgraph.outputs[i]),
reason="insert_deepcopy") reason="insert_deepcopy")
break break
else: else:
fgraph.change_input('output', i, deep_copy_op(fgraph.outputs[i]), fgraph.change_input(
'output', i,
deep_copy_op(fgraph.outputs[i]),
reason="insert_deepcopy") reason="insert_deepcopy")
break break
elif wrapped_outputs[i].borrow: elif wrapped_outputs[i].borrow:
fgraph.change_input('output', i, view_op(fgraph.outputs[i]), fgraph.change_input('output', i,
view_op(fgraph.outputs[i]),
reason="insert_deepcopy") reason="insert_deepcopy")
break break
else: else:
fgraph.change_input('output', i, deep_copy_op(fgraph.outputs[i]), fgraph.change_input('output', i,
deep_copy_op(fgraph.outputs[i]),
reason="insert_deepcopy") reason="insert_deepcopy")
break break
...@@ -866,17 +893,20 @@ class FunctionMaker(object): ...@@ -866,17 +893,20 @@ class FunctionMaker(object):
if len(input) == 2: if len(input) == 2:
return SymbolicInput(input[0], update=input[1]) return SymbolicInput(input[0], update=input[1])
else: else:
raise TypeError("Expected two elements in the list or tuple.", input) raise TypeError("Expected two elements in the list or tuple.",
input)
else: else:
raise TypeError("Unknown input type: %s (%s), expected Variable instance", type(input), input) raise TypeError("Unknown input type: %s (%s), expected Variable "
"instance", type(input), input)
@staticmethod @staticmethod
def expand_in(sinput, rinputs): def expand_in(sinput, rinputs):
# For SymbolicInputKits, this extracts a list of SymbolicInput instances # For SymbolicInputKits, this extracts a list of SymbolicInput
# and corresponding indices such that these SymbolicInputs are representative # instances and corresponding indices such that these
# of some of the Variable instances in inputs. # SymbolicInputs are representative of some of the Variable
# For SymbolicInput, this returns None as the list of indices and a list with # instances in inputs. For SymbolicInput, this returns None
# just the SymbolicInput. # as the list of indices and a list with just the
# SymbolicInput.
if isinstance(sinput, SymbolicInputKit): if isinstance(sinput, SymbolicInputKit):
return sinput.complete(rinputs) return sinput.complete(rinputs)
elif isinstance(sinput, SymbolicInput): elif isinstance(sinput, SymbolicInput):
...@@ -889,24 +919,25 @@ class FunctionMaker(object): ...@@ -889,24 +919,25 @@ class FunctionMaker(object):
elif isinstance(output, gof.Variable): elif isinstance(output, gof.Variable):
return SymbolicOutput(output) return SymbolicOutput(output)
else: else:
raise TypeError("Unknown output type: %s (%s)", type(output), output) raise TypeError("Unknown output type: %s (%s)", type(output),
output)
def optimize_graph_with_cache(self, optimizer, inputs, outputs): def optimize_graph_with_cache(self, optimizer, inputs, outputs):
# This function is not finished # This function is not finished
from theano.gof.compilelock import get_lock, release_lock from theano.gof.compilelock import get_lock, release_lock
import os.path import os.path
graph_db_file = os.path.join(theano.config.compiledir, 'optimized_graphs.pkl') graph_db_file = os.path.join(theano.config.compiledir,
'optimized_graphs.pkl')
# the inputs, outputs, and size of the graph to be optimized # the inputs, outputs, and size of the graph to be optimized
inputs_new = [inp.variable for inp in inputs] inputs_new = [inp.variable for inp in inputs]
outputs_new = [out.variable for out in outputs] outputs_new = [out.variable for out in outputs]
size_new = len(self.fgraph.apply_nodes) size_new = len(self.fgraph.apply_nodes)
need_optimize = False
get_lock() get_lock()
key = None
# Beginning of cache optimizations. # Beginning of cache optimizations.
# Could be refactored in different functions. # Could be refactored in different functions.
def load_graph_db(): def load_graph_db():
if os.path.isfile(graph_db_file): if os.path.isfile(graph_db_file):
print('graph_db already exists') print('graph_db already exists')
...@@ -919,8 +950,9 @@ class FunctionMaker(object): ...@@ -919,8 +950,9 @@ class FunctionMaker(object):
# load the graph_db dictionary # load the graph_db dictionary
try: try:
f = open(graph_db_file, 'rb') f = open(graph_db_file, 'rb')
# Temporary hack to allow theano.scan_module.tests.test_scan.T_Scan # Temporary hack to allow
# to finish. Should be changed in definitive version. # theano.scan_module.tests.test_scan.T_Scan to
# finish. Should be changed in definitive version.
tmp = theano.config.unpickle_function tmp = theano.config.unpickle_function
theano.config.unpickle_function = False theano.config.unpickle_function = False
graph_db = cPickle.load(f) graph_db = cPickle.load(f)
...@@ -961,16 +993,21 @@ class FunctionMaker(object): ...@@ -961,16 +993,21 @@ class FunctionMaker(object):
# two graphs are for sure different # two graphs are for sure different
print('need to optimize, because output size is different') print('need to optimize, because output size is different')
continue continue
elif not all(input_new.type == input_old.type for elif not all(input_new.type == input_old.type
input_new, input_old in zip(inputs_new, inputs_old)): for input_new, input_old in
print('need to optimize, because inputs are of different types') zip(inputs_new, inputs_old)):
print('need to optimize, because inputs are of different '
'types')
continue continue
elif not all(output_new.type == output_old.type for elif not all(output_new.type == output_old.type
output_new, output_old in zip(outputs_new, outputs_old)): for output_new, output_old in
print('need to optimize, because outputs are of different types') zip(outputs_new, outputs_old)):
print('need to optimize, because outputs are of different '
'types')
continue continue
elif not size_old == size_new: elif not size_old == size_new:
print('need to optimize, because numbers of nodes in graph are different') print('need to optimize, because numbers of nodes in graph'
' are different')
continue continue
else: else:
flags = [] flags = []
...@@ -1032,7 +1069,8 @@ class FunctionMaker(object): ...@@ -1032,7 +1069,8 @@ class FunctionMaker(object):
return found_graph_in_db return found_graph_in_db
graph_db = load_graph_db() graph_db = load_graph_db()
print('loaded graph_db from %s, size=%d' % (graph_db_file, len(graph_db))) print('loaded graph_db from %s, size=%d' % (graph_db_file,
len(graph_db)))
found_graph = find_same_graph_in_db(graph_db) found_graph = find_same_graph_in_db(graph_db)
if found_graph: if found_graph:
self.fgraph = found_graph self.fgraph = found_graph
...@@ -1043,7 +1081,7 @@ class FunctionMaker(object): ...@@ -1043,7 +1081,7 @@ class FunctionMaker(object):
self.fgraph.variables = set(gof.graph.variables( self.fgraph.variables = set(gof.graph.variables(
self.fgraph.inputs, self.fgraph.outputs)) self.fgraph.inputs, self.fgraph.outputs))
# check_integrity parameters was added to ignore # check_integrity parameters was added to ignore
#"excess cached variables" errors. Works that way # "excess cached variables" errors. Works that way
# but once again the error couldbe worth # but once again the error couldbe worth
# investigating. # investigating.
before_opt = self.fgraph.clone(check_integrity=False) before_opt = self.fgraph.clone(check_integrity=False)
...@@ -1063,16 +1101,18 @@ class FunctionMaker(object): ...@@ -1063,16 +1101,18 @@ class FunctionMaker(object):
""" """
:type inputs: a list of SymbolicInput instances :type inputs: a list of SymbolicInput instances
:type outputs: a list of SymbolicOutput instances
outputs may also be a single Variable (not a list), in which
case the functions produced by FunctionMaker will return
their output value directly
:param mode: a Mode instance telling FunctionMaker how to optimize and link. None :type outputs: a list of SymbolicOutput instances outputs may
means to use the `config.mode`. also be a single Variable (not a list), in which case the
functions produced by FunctionMaker will return their
output value directly
:param mode: a Mode instance telling FunctionMaker how to
optimize and link. None means to use the `config.mode`.
:param accept_inplace: True iff it is acceptable to have inplace operations :param accept_inplace: True iff it is acceptable to have
in the graph from the inputs to the outputs inplace operations in the graph from the inputs to the
outputs
:param on_unused_input: What to do if a variable in the 'inputs' list :param on_unused_input: What to do if a variable in the 'inputs' list
is not used in the graph. Possible values are: is not used in the graph. Possible values are:
...@@ -1098,9 +1138,10 @@ class FunctionMaker(object): ...@@ -1098,9 +1138,10 @@ class FunctionMaker(object):
# This is very important: # This is very important:
# 1) We preload the cache here to don't have its timming # 1) We preload the cache here to don't have its timming
# included in optimization that compile function. # included in optimization that compile function.
# 2) Do not refresh the cache here by default. It cause too much # 2) Do not refresh the cache here by default. It cause
# execution time during testing as we compile much more functions # too much execution time during testing as we compile
# then the number of compile c module. # much more functions then the number of compile c
# module.
theano.gof.cc.get_module_cache().refresh() theano.gof.cc.get_module_cache().refresh()
# Handle the case where inputs and/or outputs is a single # Handle the case where inputs and/or outputs is a single
# Variable (not in a list) # Variable (not in a list)
...@@ -1117,21 +1158,27 @@ class FunctionMaker(object): ...@@ -1117,21 +1158,27 @@ class FunctionMaker(object):
inputs = [inputs] inputs = [inputs]
# Wrap them in In or Out instances if needed. # Wrap them in In or Out instances if needed.
inputs, outputs = map(self.wrap_in, inputs), map(self.wrap_out, outputs) inputs = map(self.wrap_in, inputs)
_inputs = gof.graph.inputs([o.variable for o in outputs] + [i.update outputs = map(self.wrap_out, outputs)
for i in inputs if getattr(i, 'update', False)]) _inputs = gof.graph.inputs([o.variable for o in outputs] +
[i.update for i in inputs
if getattr(i, 'update', False)])
# Check if some input variables are unused # Check if some input variables are unused
self._check_unused_inputs(inputs, outputs, on_unused_input) self._check_unused_inputs(inputs, outputs, on_unused_input)
# Make a list of (SymbolicInput|SymblicInputKits, indices, [SymbolicInput,...]), one # Make a list of (SymbolicInput|SymblicInputKits, indices,
# tuple for each input. (See Function.indices for more details) # [SymbolicInput,...]), one tuple for each input. (See
indices = [[input] + self.expand_in(input, _inputs) for input in inputs] # Function.indices for more details)
indices = [[input] + self.expand_in(input, _inputs)
for input in inputs]
if fgraph is None: if fgraph is None:
need_opt = True need_opt = True
# make the fgraph (copies the graph, creates NEW INPUT AND OUTPUT VARIABLES) # make the fgraph (copies the graph, creates NEW INPUT AND
fgraph, additional_outputs = std_fgraph(inputs, outputs, accept_inplace) # OUTPUT VARIABLES)
fgraph, additional_outputs = std_fgraph(inputs, outputs,
accept_inplace)
fgraph.profile = profile fgraph.profile = profile
else: else:
# fgraph is already an optimized one # fgraph is already an optimized one
...@@ -1149,7 +1196,8 @@ class FunctionMaker(object): ...@@ -1149,7 +1196,8 @@ class FunctionMaker(object):
# Why we add stack on node when it get done in output var? # Why we add stack on node when it get done in output var?
try: try:
# optimize the fgraph # optimize the fgraph
theano.config.compute_test_value = theano.config.compute_test_value_opt theano.config.compute_test_value = \
theano.config.compute_test_value_opt
theano.config.traceback.limit = 0 theano.config.traceback.limit = 0
start_optimizer = time.time() start_optimizer = time.time()
...@@ -1165,7 +1213,8 @@ class FunctionMaker(object): ...@@ -1165,7 +1213,8 @@ class FunctionMaker(object):
if profile: if profile:
profile.optimizer_time += opt_time profile.optimizer_time += opt_time
if theano.config.profile_optimizer: if theano.config.profile_optimizer:
profile.optimizer_profile = (optimizer, optimizer_profile) profile.optimizer_profile = (optimizer,
optimizer_profile)
_logger.debug('Optimizing took %f seconds', opt_time) _logger.debug('Optimizing took %f seconds', opt_time)
# Add deep copy to respect the memory interface # Add deep copy to respect the memory interface
...@@ -1176,14 +1225,19 @@ class FunctionMaker(object): ...@@ -1176,14 +1225,19 @@ class FunctionMaker(object):
# initialize the linker # initialize the linker
if not hasattr(linker, 'accept'): if not hasattr(linker, 'accept'):
raise ValueError("'linker' parameter of FunctionMaker should be a Linker with an accept method " \ raise ValueError("'linker' parameter of FunctionMaker should be "
"or one of %s" % theano.compile.mode.predefined_linkers.keys()) "a Linker with an accept method or one of %s" %
theano.compile.mode.predefined_linkers.keys())
# the 'no_borrow' outputs are the ones for which that we can't return the internal storage pointer. # the 'no_borrow' outputs are the ones for which that we can't
# return the internal storage pointer.
assert len(fgraph.outputs) == len(outputs + additional_outputs) assert len(fgraph.outputs) == len(outputs + additional_outputs)
no_borrow = [output for output, spec in zip(fgraph.outputs, outputs + additional_outputs) if not spec.borrow] no_borrow = [output for output, spec in
zip(fgraph.outputs, outputs + additional_outputs)
if not spec.borrow]
if no_borrow: if no_borrow:
self.linker = linker.accept(fgraph, no_recycling=infer_reuse_pattern(fgraph, no_borrow)) self.linker = linker.accept(
fgraph, no_recycling=infer_reuse_pattern(fgraph, no_borrow))
else: else:
self.linker = linker.accept(fgraph) self.linker = linker.accept(fgraph)
...@@ -1209,8 +1263,7 @@ class FunctionMaker(object): ...@@ -1209,8 +1263,7 @@ class FunctionMaker(object):
(i.value is not None and (i.value is not None and
not isinstance(i.value, gof.Container) and not isinstance(i.value, gof.Container) and
i.update is None) i.update is None)
for i in self.inputs for i in self.inputs]
]
def _check_unused_inputs(self, inputs, outputs, on_unused_input): def _check_unused_inputs(self, inputs, outputs, on_unused_input):
if on_unused_input is None: if on_unused_input is None:
...@@ -1241,39 +1294,46 @@ class FunctionMaker(object): ...@@ -1241,39 +1294,46 @@ class FunctionMaker(object):
for i in inputs: for i in inputs:
if ((i.variable not in used_inputs) and (i.update is None)): if ((i.variable not in used_inputs) and (i.update is None)):
if on_unused_input == 'warn': if on_unused_input == 'warn':
warnings.warn(msg % (inputs.index(i), i.variable, warn_msg), stacklevel=6) warnings.warn(msg % (inputs.index(i), i.variable,
warn_msg), stacklevel=6)
elif on_unused_input == 'raise': elif on_unused_input == 'raise':
raise UnusedInputError(msg % (inputs.index(i), i.variable, err_msg)) raise UnusedInputError(msg % (inputs.index(i),
i.variable, err_msg))
else: else:
raise ValueError(("Invalid value for keyword " raise ValueError("Invalid value for keyword "
"on_unused_input of theano.function: '%s'. " "on_unused_input of theano.function: "
"valid values are 'raise', 'warn', and 'ignore'." "'%s'.\nValid values are 'raise', "
% on_unused_input)) "'warn', and 'ignore'." % on_unused_input)
def create(self, input_storage=None, trustme=False): def create(self, input_storage=None, trustme=False):
""" """
Create a function. Create a function.
input_storage -> a list matching the inputs list and providing default values input_storage -> a list matching the inputs list and providing
if the default for an input is None, then that input is a default values if the default for an input is
required input. For an input with an update, the default None, then that input is a required input. For an
acts as initialization. input with an update, the default acts as
initialization.
trustme -> disables some exceptions, used internally trustme -> disables some exceptions, used internally
""" """
if input_storage is None: if input_storage is None:
input_storage = [None] * len(self.inputs) input_storage = [None] * len(self.inputs)
input_storage_lists = [] # list of independent one-element lists, will be passed to the linker # list of independent one-element lists, will be passed to the linker
input_storage_lists = []
defaults = [] defaults = []
# The following loop is to fill in the input_storage_lists and defaults lists. # The following loop is to fill in the input_storage_lists and
# defaults lists.
assert len(self.indices) == len(input_storage) assert len(self.indices) == len(input_storage)
for i, ((input, indices, subinputs), input_storage_i) in enumerate(zip(self.indices, input_storage)): for i, ((input, indices, subinputs), input_storage_i) in \
enumerate(zip(self.indices, input_storage)):
# Replace any default value given as a variable by its container.
# Note that this makes sense only in the context of shared variables, # Replace any default value given as a variable by its
# but for now we avoid dealing directly with them to avoid dependency # container. Note that this makes sense only in the
# on the shared variables work-in-progress repository. # context of shared variables, but for now we avoid
# dealing directly with them to avoid dependency on the
# shared variables work-in-progress repository.
if isinstance(input_storage_i, gof.Variable): if isinstance(input_storage_i, gof.Variable):
input_storage_i = input_storage_i.container input_storage_i = input_storage_i.container
...@@ -1282,7 +1342,8 @@ class FunctionMaker(object): ...@@ -1282,7 +1342,8 @@ class FunctionMaker(object):
# share the same storage. This is done by appending # share the same storage. This is done by appending
# input_storage_i.storage to input_storage_lists. # input_storage_i.storage to input_storage_lists.
if indices is not None: if indices is not None:
raise TypeError("Cannot take a Container instance as default for a SymbolicInputKit.") raise TypeError("Cannot take a Container instance as "
"default for a SymbolicInputKit.")
input_storage_lists.append(input_storage_i.storage) input_storage_lists.append(input_storage_i.storage)
storage = input_storage[i].storage[0] storage = input_storage[i].storage[0]
...@@ -1295,7 +1356,8 @@ class FunctionMaker(object): ...@@ -1295,7 +1356,8 @@ class FunctionMaker(object):
required = self.required[i] required = self.required[i]
refeed = self.refeed[i] refeed = self.refeed[i]
# sanity check-- if an input is required it should not need to be refed # sanity check-- if an input is required it should not
# need to be refed
assert not (required and refeed) assert not (required and refeed)
# shared variables need neither be input by the user nor refed # shared variables need neither be input by the user nor refed
...@@ -1312,9 +1374,7 @@ class FunctionMaker(object): ...@@ -1312,9 +1374,7 @@ class FunctionMaker(object):
if storage is not None: if storage is not None:
assert refeed or not required assert refeed or not required
defaults.append((required, defaults.append((required, refeed, storage))
refeed,
storage))
# Get a function instance # Get a function instance
start_linker = time.time() start_linker = time.time()
...@@ -1338,7 +1398,8 @@ class FunctionMaker(object): ...@@ -1338,7 +1398,8 @@ class FunctionMaker(object):
self.profile.import_time += import_time self.profile.import_time += import_time
fn = self.function_builder(_fn, _i, _o, self.indices, self.outputs, fn = self.function_builder(_fn, _i, _o, self.indices, self.outputs,
defaults, self.unpack_single, self.return_none, self.output_keys, self) defaults, self.unpack_single,
self.return_none, self.output_keys, self)
fn.profile = self.profile fn.profile = self.profile
return fn return fn
...@@ -1367,19 +1428,6 @@ def _constructor_FunctionMaker(kwargs): ...@@ -1367,19 +1428,6 @@ def _constructor_FunctionMaker(kwargs):
copy_reg.pickle(FunctionMaker, _pickle_FunctionMaker) copy_reg.pickle(FunctionMaker, _pickle_FunctionMaker)
try:
# Pickle of slice is implemented on python 2.6. To enabled be
# compatible with python 2.4, we implement pickling of slice
# ourself.
cPickle.dumps(slice(0, 10, 100))
except TypeError:
# This slice pickle implementation seam backward and forward compatible.
def _pickle_slice(s):
return (slice, (s.start, s.stop, s.step))
copy_reg.pickle(slice, _pickle_slice)
__checkers = [] __checkers = []
...@@ -1390,7 +1438,6 @@ def check_equal(x, y): ...@@ -1390,7 +1438,6 @@ def check_equal(x, y):
except Exception: except Exception:
continue continue
return x == y return x == y
#raise Exception('No checker for equality between %s and %s' % (x, y))
def register_checker(checker): def register_checker(checker):
...@@ -1405,10 +1452,10 @@ def orig_function(inputs, outputs, mode=None, accept_inplace=False, ...@@ -1405,10 +1452,10 @@ def orig_function(inputs, outputs, mode=None, accept_inplace=False,
:param inputs: list of `SymbolicInput` or `In` instances :param inputs: list of `SymbolicInput` or `In` instances
:param outputs: a SymbolicOutput or a list of `SymbolicOutput` or `Out` :param outputs: a SymbolicOutput or a list of `SymbolicOutput` or
instances. The return value of the returned function will match the `Out` instances. The return value of the returned function
format of this argument (either the value itself or a list of one or more will match the format of this argument (either the value
return values) itself or a list of one or more return values)
:param mode: a descriptive string or a Mode instance. (Default of None :param mode: a descriptive string or a Mode instance. (Default of None
means to use `config.mode` (See below for descriptive string list). means to use `config.mode` (See below for descriptive string list).
...@@ -1422,7 +1469,8 @@ def orig_function(inputs, outputs, mode=None, accept_inplace=False, ...@@ -1422,7 +1469,8 @@ def orig_function(inputs, outputs, mode=None, accept_inplace=False,
- FAST_COMPILE (minimal optimization) - FAST_COMPILE (minimal optimization)
- ProfileMode(deprecated): allow to print a profile mode with mode.print_summary - ProfileMode(deprecated): allow to print a profile mode with
mode.print_summary
- DebugMode: verify many internal conditions that are normally assumed - DebugMode: verify many internal conditions that are normally assumed
(slow) (slow)
...@@ -1471,7 +1519,7 @@ def orig_function(inputs, outputs, mode=None, accept_inplace=False, ...@@ -1471,7 +1519,7 @@ def orig_function(inputs, outputs, mode=None, accept_inplace=False,
accept_inplace=accept_inplace, accept_inplace=accept_inplace,
profile=profile, profile=profile,
on_unused_input=on_unused_input, on_unused_input=on_unused_input,
output_keys = output_keys).create( output_keys=output_keys).create(
defaults) defaults)
t2 = time.time() t2 = time.time()
...@@ -1590,15 +1638,15 @@ def get_info_on_inputs(named_inputs, n_unnamed_inputs): ...@@ -1590,15 +1638,15 @@ def get_info_on_inputs(named_inputs, n_unnamed_inputs):
"constructor to give it a name)." % n_unnamed_inputs) "constructor to give it a name)." % n_unnamed_inputs)
else: else:
if n_unnamed_inputs == 0: if n_unnamed_inputs == 0:
msg = ("The function has %s named input%s (%s)." % ( msg = ("The function has %s named input%s (%s)." %
n_named_inputs, get_plural(n_named_inputs), (n_named_inputs, get_plural(n_named_inputs),
', '.join(named_inputs))) ', '.join(named_inputs)))
else: else:
msg = ("The function has %s named input%s (%s), and %s unnamed " msg = ("The function has %s named input%s (%s), and %s unnamed "
"input%s which thus cannot be accessed through keyword " "input%s which thus cannot be accessed through keyword "
"argument%s (use 'name=...' in a variable's constructor " "argument%s (use 'name=...' in a variable's constructor "
"to give it a name)." % ( "to give it a name)." %
n_named_inputs, get_plural(n_named_inputs), (n_named_inputs, get_plural(n_named_inputs),
', '.join(named_inputs), n_unnamed_inputs, ', '.join(named_inputs), n_unnamed_inputs,
get_plural(n_unnamed_inputs), get_plural(n_unnamed_inputs),
get_plural(n_unnamed_inputs))) get_plural(n_unnamed_inputs)))
......
...@@ -40,7 +40,6 @@ whitelist_flake8 = [ ...@@ -40,7 +40,6 @@ whitelist_flake8 = [
"tests/unittest_tools.py", "tests/unittest_tools.py",
"compile/__init__.py", "compile/__init__.py",
"compile/profiling.py", "compile/profiling.py",
"compile/function_module.py",
"compile/monitormode.py", "compile/monitormode.py",
"compile/tests/test_builders.py", "compile/tests/test_builders.py",
"compile/tests/test_misc.py", "compile/tests/test_misc.py",
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论